Dify 高级实验(01):智能文档处理——如何把非结构化文档变成结构化数据?
1. 业务场景
先讲一个我们实际遇到的场景。
一家中型企业的人力资源部,招聘专员小李每天早上打开邮箱,里面躺着十几份新简历——有的是招聘平台导出的纯文本,有的是 PDF 转出来的乱格式,还有的直接是一段自我介绍。她要做的第一件事,是把每份简历里的姓名、年龄、技能、经验年限、期望薪资、学历逐项拆出来,手动录进招聘系统,系统才能按岗位要求做初筛。一份简历从读完到录完,快则几分钟,慢的要来回翻原文确认字段。赶上校招季,一天几十份简历,大半天就耗在「打字」上了。
我们第一次接这类需求时,第一反应也是「上 NLP 解析库,正则加模型一起上」。后来把需求拆开看才发现——简历里的字段就六七个,难点根本不在「拆」,而在「懂」:「5 年经验」和「五年」要归一成同一个数,「毕业 3 年」要能推算出经验年限。而「懂」恰恰是 LLM 的强项。Dify 工作流里的参数提取器(Parameter Extractor)就是干这个的:声明好字段,粘贴原文,直接输出 JSON,解析代码一行都不用写。
这不是个例。任何「把非结构化文档变成结构化数据」的场景都是这个模式:合同管理员收到供应商合同,要人工录金额、期限、付款条件;财务月底面对几百张发票,要逐张录发票号、税额;客服把客户邮件里的诉求抄进工单系统,再人工分类打标签。文档换了,动作一模一样——机械地拆字段、录入、核对。
2. 场景痛点
这个流程的痛点,在招聘专员身上体现得最直接:
- 人工录入慢,慢在没价值:一份简历 5-10 分钟,几十份就是大半天——录入本身不产生任何判断,却是每天躲不掉的固定动作,招聘专员的时间被「打字」挤占,真正该做的候选人沟通和面试安排反而往后排。
- 录入口径不统一,数据一录就脏:「5 年经验」「五年经验」「5 年+」说的是同一件事,不同人录进系统变成三种写法,后续按经验年限筛选时本该命中的简历被漏掉,统计报表也失真。
- 漏字段静默发生:一份简历漏了学历,录入时没人发现,直到按学历过滤时这条记录才「消失」——问题在录入几天后才暴露,还得回头翻原始简历补录。
- 数据不可追溯:这条记录是谁录的、从哪份文档来的、录进去的字段对不对,全都没有留痕,出了差错只能人肉排查。
本质上,最机械、最不该花人力的「录入」环节,恰恰卡住了整个数字化流程的效率——问题不是缺人,而是缺一条把非结构化文档自动变成结构化数据的流水线。
3. 方案:为什么是这套文档结构化流水线
Dify 工作流里的「参数提取 → 数据校验 → 条件分支 → 模拟入库 → LLM 回复」链路,正好把这段机械劳动自动化。
选它的理由,我们实际对比过:
- 参数提取器是平台原生能力:不用自研 NLP 解析器,节点里声明好字段(姓名/年龄/技能/经验/薪资/学历),LLM 自动从自然语言里抽出来输出 JSON,语义理解(「毕业 3 年」能推算出经验年限)也一并解决;
- 校验规则代码化,坏数据进不了库:缺字段、年龄越界这类问题,用代码节点在写入前二次校验并给出明确报错,而不是带着残缺数据往下走;
- 入库与回复分离,流程可扩展:模拟入库节点生成记录编号,LLM 节点把结果包装成面向用户的友好回复——将来接真实数据库,只需把模拟入库换成 HTTP 节点。
这篇文章我们就用它搭一个「简历自动入库」系统:粘贴一段简历文本,自动提取 6 个字段,校验通过后模拟写入数据库,返回记录编号与信息摘要。
4. 整体架构
链路很清晰:入口收文档原文 → 提取结构化字段 → 校验完整性 → 分支处理。校验和分支是这个架构的关键——参数提取偶尔漏字段是 LLM 节点的常态,不校验直接「入库」,残缺数据就会带着空姓名、空技能写进数据库。
5. 模块设计
5.1 参数提取器 pe_extract
query 引用开始节点原文,模型用 DeepSeek 并开
reasoning_mode: function_call 保证结构化输出:
query: [start, document_text]
model: deepseek-v4-flash # temperature 0.01,输出稳定
reasoning_mode: function_call
parameters:
- name: name # string, required 姓名
- name: age # number 年龄(可从"毕业X年"推算)
- name: skills # array[string], required 技能列表
- name: experience_years # number, required 工作经验(年)
- name: expected_salary # number 期望薪资
- name: education # string 最高学历提取指令要点:技能以数组形式提取、经验以年为单位、没有明确信息填 null 而不是瞎编。
5.2 代码节点 cd_validate(校验 + 准备写入)
对 PE 结果做二次校验,并拼出 record_text 供 LLM
直接引用:
def main(name, age, skills, experience_years, expected_salary, education):
issues = []
if not name:
issues.append("缺少姓名")
if not skills or len(skills or []) == 0:
issues.append("缺少技能")
if age and (age < 16 or age > 70):
issues.append("年龄 {} 异常".format(age))
record = {
"name": name or "未知",
"age": int(age) if age else None,
"skills": skills or [],
"experience_years": float(experience_years) if experience_years else 0,
"expected_salary": float(expected_salary) if expected_salary else None,
"education": education or "",
}
record_text = "姓名:{} | 年龄:{} | 技能:{} | 经验:{}年 | 期望薪资:{} | 学历:{}".format(
record["name"], record["age"], "、".join(record["skills"]),
record["experience_years"], record["expected_salary"], record["education"])
return {
"valid": "true" if len(issues) == 0 else "false",
"issues": issues, "record_data": record, "record_text": record_text,
"error_text": "信息不完整:" + "、".join(issues) if issues else ""
}5.3 条件分支 cond_valid
注意 boolean 展平:代码节点返回的 valid 是字符串
"true"/"false",if-else 用 is
比较字符串:
case_valid: cd_validate.valid is "true"
case_invalid: cd_validate.valid is "false"5.4 模拟入库 cd_write 与 LLM 回复 lm_reply
cd_write 模拟写入延迟并生成记录 ID(CAN- +
时间戳取模);lm_reply
的系统提示词必须用三花括号引用上游字段:
你是一个文件处理助手。用户提交了一份文档,系统已从中提取信息并存入数据库。
请根据以下信息回复用户,告知处理结果:
提取的信息:{{#cd_validate.record_text#}}
数据库记录编号:{{#cd_write.record_id#}}
入库时间:{{#cd_write.timestamp#}}
回复要友好、专业,包含提取到的关键信息和记录编号。
结束节点:end_ok 输出 reply ←
lm_reply.text;end_bad 输出 error
← cd_validate.error_text。
6. 运行验证
| 输入 | 期望行为 | 实测 |
|---|---|---|
| 「张三,28岁,计算机本科,5年 Python/Java/Go 经验,期望薪资 25K-30K。」 | 提取 6 字段校验通过,模拟入库,返回记录编号与信息摘要 | 与预期一致,回复包含 CAN- 编号 |
| 「你好」 | 提取缺姓名/技能,走 end_bad,返回「信息不完整:缺少姓名、缺少技能」 | 与预期一致,未触发入库 |
| 「李四,50岁,20年经验,学历大专」 | 年龄/经验正常但缺技能,校验拦截 | 与预期一致(skills 为 required) |
7. 实战坑
| 坑 | 现象 | 修复 |
|---|---|---|
LLM prompt 用双花括号
{{变量名}} |
1.16
不替换双花括号,字面传给模型,回复中出现
{{#cd_write.record_id#}} 原文 |
一律用三花括号
{{#节点id.字段#}}(dify103_01 实测) |
| 代码节点返回 boolean 类型 | valid
在变量选择器里不可见,if-else 无法引用 |
代码里转字符串
"true"/"false",if-else 用 is
比较字符串 |
PE 缺
reasoning_mode: function_call |
参数提取偶尔输出不完整 JSON 或类型不符(skills 变字符串) | 显式配置 function_call + temperature 0.01 稳定输出(dify103_01 实测) |
| DeepSeek 推理模型未配 separated | 思考过程混入 text
输出,回复里出现推理碎屑 |
LLM 节点开
reasoning_format: separated,思考与回答分离 |
8. 实验文档及源码获取
- 实验文档(完整操作步骤):DIFY-103-01:智能文档处理系统.md
- 源码(可直接导入):dify103_01_智能文档处理系统.yml
文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。
- Dify 高级实验(01):智能文档处理——如何把非结构化文档变成结构化数据?
- Dify 高级实验(02):对话式 BI 分析——如何让业务人员用自然语言直接查数?
- Dify 高级实验(03):智能客服——如何让 AI 自动应答并把疑难问题转人工?
- Dify 高级实验(04):RAG 增强问答——如何让知识库问答更准还能多轮追问?
- Dify 高级实验(05):多步推理——如何把复杂问题拆解成子问题逐个击破?
- Dify 高级实验(06):自动化报告——如何让系统定时自动生成周报?
- Dify 高级实验(07):智能审批——如何让机器自动完成流程审批?
- Dify 高级实验(08):智能告警——如何用 AI 替代固定阈值监控?
- Dify 高级实验(09):测试用例生成——如何让 AI 自动产出测试用例?
- Dify 高级实验(10):综合实战——如何从零搭建一个企业级智能客服平台?