← 返回文章列表

Dify 高级实验(01):智能文档处理——如何把非结构化文档变成结构化数据?

1. 业务场景

先讲一个我们实际遇到的场景。

一家中型企业的人力资源部,招聘专员小李每天早上打开邮箱,里面躺着十几份新简历——有的是招聘平台导出的纯文本,有的是 PDF 转出来的乱格式,还有的直接是一段自我介绍。她要做的第一件事,是把每份简历里的姓名、年龄、技能、经验年限、期望薪资、学历逐项拆出来,手动录进招聘系统,系统才能按岗位要求做初筛。一份简历从读完到录完,快则几分钟,慢的要来回翻原文确认字段。赶上校招季,一天几十份简历,大半天就耗在「打字」上了。

我们第一次接这类需求时,第一反应也是「上 NLP 解析库,正则加模型一起上」。后来把需求拆开看才发现——简历里的字段就六七个,难点根本不在「拆」,而在「懂」:「5 年经验」和「五年」要归一成同一个数,「毕业 3 年」要能推算出经验年限。而「懂」恰恰是 LLM 的强项。Dify 工作流里的参数提取器(Parameter Extractor)就是干这个的:声明好字段,粘贴原文,直接输出 JSON,解析代码一行都不用写。

这不是个例。任何「把非结构化文档变成结构化数据」的场景都是这个模式:合同管理员收到供应商合同,要人工录金额、期限、付款条件;财务月底面对几百张发票,要逐张录发票号、税额;客服把客户邮件里的诉求抄进工单系统,再人工分类打标签。文档换了,动作一模一样——机械地拆字段、录入、核对

2. 场景痛点

这个流程的痛点,在招聘专员身上体现得最直接:

本质上,最机械、最不该花人力的「录入」环节,恰恰卡住了整个数字化流程的效率——问题不是缺人,而是缺一条把非结构化文档自动变成结构化数据的流水线。

3. 方案:为什么是这套文档结构化流水线

Dify 工作流里的「参数提取 → 数据校验 → 条件分支 → 模拟入库 → LLM 回复」链路,正好把这段机械劳动自动化。

选它的理由,我们实际对比过:

这篇文章我们就用它搭一个「简历自动入库」系统:粘贴一段简历文本,自动提取 6 个字段,校验通过后模拟写入数据库,返回记录编号与信息摘要。

4. 整体架构

graph TD start["开始:document_text / document_type"] --> pe["参数提取器 pe_extract:提取 6 字段 JSON"] pe --> cv["代码节点 cd_validate:校验 + 准备写入数据"] cv --> cond{"条件分支 cond_valid"} cond -- "通过" --> cw["代码节点 cd_write:模拟入库,生成记录 ID"] cw --> lm["LLM lm_reply:生成处理结果回复"] lm --> eok["结束 end_ok:输出 reply"] cond -- "失败" --> ebad["结束 end_bad:输出 error 错误提示"]

链路很清晰:入口收文档原文 → 提取结构化字段 → 校验完整性 → 分支处理。校验和分支是这个架构的关键——参数提取偶尔漏字段是 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 输出 replylm_reply.textend_bad 输出 errorcd_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. 实验文档及源码获取

文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。

联系我

15088711270

手机端点击号码可直接拨打 · 桌面端可复制

微信二维码

扫码加微信 · 备注「门户」更快通过