Dify 中级实验(03):模板转换实战——如何用零 Token 完成文本加工?
1. 业务场景
先讲一个我们实际遇到的场景。
一家电商公司的运营团队,每天要给不同角色出「评论汇总」:给管理层的是摘要,给产品组的是分析报告,给商家的是一封邮件通知。底层数据都一样——数据库/API 返回的一堆评价 JSON,但输出格式有三种,每天人工复制粘贴、调整排版,动作重复且毫无技术含量。
我们第一次接这类需求时,第一反应也是「丢给 LLM 生成,反正模型会写」。真正动手才发现——LLM 生成的格式十次有三次走样,字段偶尔丢,还得反复调 prompt 盯着。后来翻 Dify 的节点列表才发现:平台原生的 Template Transform(模板转换) 就是干这个的——基于 Jinja2 的文本加工,不消耗 LLM Token,格式永远可控。
这不是个例。任何「数据是结构化的、输出格式是固定的、内容不需要理解语义」的文本加工场景都是这个模式:日报/周报自动生成、邮件与通知模板、订单确认信、报表摘要……格式确定、规则明确,就该用模板。
2. 场景痛点
这个流程的痛点,在运营身上体现得最直接:
- 重复劳动:同一份数据每天按三种格式各排一遍,换数据源就要重调格式,时间全耗在排版上——产出没有增量,排版本身也不产生价值,却每天吃掉最好的几个小时。
- 用 LLM 生成不可控:让大模型按固定格式生成,偶尔格式走样、字段缺失,还得反复调 prompt 盯着——每次调用都烧 Token,格式还不一定对——把「确定的格式」交给「会发挥的模型」,本身就是赌。
- 时效性差:汇总要等 LLM 逐条生成,数据量大时用户等不起,批量场景更明显——昨天的数据今天才出报告。
- 成本持续累积:每份报告都烧 Token,日积月累是一笔实实在在的开销——为「确定的格式」付「理解的费用」,不划算。
本质上,规则明确的文本组装用 LLM 是杀鸡用牛刀——钱和时间都花在了 LLM 不该干的活上。模板负责结构,LLM 负责内容,各干各的才不浪费。
3. 方案:为什么是模板转换
选模板转换的理由,我们实际对比过:
- 零成本:纯模板渲染,毫秒级出结果、Token 消耗为 0——对比 LLM 生成同格式文本的耗时与成本,差距是数量级的;
- 格式永远可控:模板即格式,输出结构完全符合预期,不存在 LLM 的「自由发挥」;
- 与 LLM 互补:格式确定、规则明确的文本组装用模板,只有需要「理解语义」的加工才用 LLM——同一个工作流里两者配合:模板负责结构,LLM 负责内容。
这篇文章我们就用它搭一个「电商评论汇总器」:一份评价 JSON,按
summary/report/email
三种格式类型输出对应文本。
4. 整体架构
链路很清晰:入口收数据 → 解析结构化 → 按格式类型走对应模板。纯 Jinja2 渲染,全程零 Token——只有数据准备(解析评价 JSON、生成采集时间)用了两个代码节点。
5. 模块设计
5.1 输入解析(Code)
reviews 是 JSON 字符串,先解析成结构化数据(同时做空值兜底):
def main(reviews: str) -> dict:
import json
try:
data = json.loads(reviews or "[]")
except Exception:
data = []
if isinstance(data, dict):
data = data.get("data", [])
return {"reviews": data, "count": len(data)}5.2 模板转换节点(核心)
摘要模板——空值保护 + 平均分计算 + 评分分布:
{% set rr = reviews if reviews else [] %}
【{{ product_name }}】用户评价摘要(共 {{ rr | length }} 条)
{% set total_rating = rr | map(attribute='rating') | sum %}
{% set avg_rating = (total_rating / rr | length) | round(1) if rr | length > 0 else 0 %}
平均评分:{{ avg_rating }} / 5
{% for r in rr %}
- {{ r.user }}({{ r.rating }}星):{{ r.content }}
{% endfor %}
报告模板——评分分布统计 + 按评分排序:
{% set rr = reviews if reviews else [] %}
# {{ product_name }} 用户评价分析报告
## 概览
- 评价总数:{{ rr | length }}
- 数据采集时间:{{ report_time }}
## 评分分布
{% set ratings = [5,4,3,2,1] %}
{% for r in ratings %}
{% set count = rr | selectattr('rating', 'equalto', r) | list | length %}
{{ r }} 星:{{ count }} 条
{% endfor %}
5.3 采集时间(Code)
⚠️ Jinja2 模板里没有 now() 函数——Dify 模板环境不提供时间函数。要输出时间,用上游代码节点生成再传参:
def main() -> dict:
import datetime
return {"report_time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M")}6. 运行验证
实测三种格式输出:
summary→ 摘要文本(评价数/均分/逐条)report→ 结构化报告(概览/评分分布/明细)email→ 邮件正文(主题 + 关键数据 + 收尾)
验证要点:输出内容完全由模板渲染,节点执行耗时毫秒级、Token 消耗为 0——对比 LLM 生成同格式文本的耗时与成本。
7. 实战坑
| 坑 | 现象 | 修复 |
|---|---|---|
| 模板用 now() | 运行报错(模板环境无时间函数) | 上游 Code 节点生成时间传参 |
| 空 reviews 直接遍历 | 无数据时模板报错/输出异常 | {% set rr = reviews if reviews else [] %}
空值保护 |
| 平均分除零 | reviews 为空时 1/0 报错 |
if rr | length > 0 else 0
保护 |
| 模板转换节点被 LLM 替代 | 每次请求烧 Token、耗时长 | 格式确定场景一律用模板(零成本) |
💡 选型心法:规则明确的文本组装 → 模板转换(零 Token);需要理解语义的加工 → LLM。同一个工作流里两者配合:模板负责结构,LLM 负责内容。
8. 实验文档及源码获取
- 实验文档(完整操作步骤):DIFY-04:模板转换——高级文本加工厂.md
- 源码(可直接导入):dify102_04_模板转换工厂.yml
文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。
- Dify 中级实验(01):参数提取器实战——如何从自然语言中提取结构化数据?
- Dify 中级实验(02):问题分类器——智能路由引擎如何四路分发?
- Dify 中级实验(03):模板转换实战——如何用零 Token 完成文本加工?
- Dify 中级实验(04):迭代进阶——如何批量处理数据并守住性能边界?
- Dify 中级实验(05):并行执行——如何让多路任务同时跑?
- Dify 中级实验(06):变量聚合——如何确定性合并多路分支结果?
- Dify 中级实验(07):子工作流——如何把公共逻辑做成可复用积木?
- Dify 中级实验(08):代码节点进阶——如何用标准库处理文件与数据?
- Dify 中级实验(09):HTTP 节点进阶——如何搞定认证、分页与错误重试?
- Dify 中级实验(10):知识库深度调优——如何科学评估检索质量?
- Dify 中级实验(11):高级 RAG 流水线——如何搭建多路检索与精排?
- Dify 中级实验(12):Agent 深度配置——如何让智能体自主调用工具?
- Dify 中级实验(13):多 Agent 协作——如何编排多个智能体分工干活?
- Dify 中级实验(14):对话变量与状态管理——如何让工作流记住多轮对话的状态?
- Dify 中级实验(15):条件分支高阶策略——多条件路由如何避免分支爆炸?
- Dify 中级实验(16):错误处理与降级——工作流如何有尊严地失败?
- Dify 中级实验(17):调试监控与性能优化——响应慢和 Token 超支如何定位?
- Dify 中级实验(18):插件开发入门——如何把工作流变成 Agent 可调用的工具?
- Dify 中级实验(19):综合实战——如何把 19 个实验串成一条生产级流水线?
- Dify 中级实验(20):综合实战——自动化报告生成流水线如何从数据到周报一步到位?