← 返回笔记索引

问题复盘

匹配不少,为什么最终简历仍然是空的:一次多节点生成链路复盘

复盘岗位要求已经匹配到多条候选人证据,但最终 PDF 只剩页眉的问题,分析 Planner、Writer 和 FactValidator 如何串联造成内容丢失,并给出覆盖下限和字段级校验方案。

匹配不少,为什么最终简历仍然是空的:一次多节点生成链路复盘

问题现象

在一次岗位简历生成中,匹配分析页已经显示多条强、中、弱匹配,部分岗位要求还关联了多个 Evidence ID。按直觉,生成结果至少应该包含实习和项目经历。

实际进入编辑预览页后,PDF 只有姓名、联系方式和研究方向,正文模块全部为空。

这个现象很容易让人把原因归结为“匹配算法没有生效”或“模型等待时间太短”。但数据库中的匹配报告并不为空,生成任务状态也是 completed。问题发生在匹配之后的多个节点之间。

环境与前置条件

问题出现在一个固定流水线的简历 Agent 中:

匹配 → 规划 → 写作 → 事实校验 → 模板渲染 → PDF

每个节点都保存结构化结果。最终 PDF 只能使用通过事实校验的 ResumeDocument,因此“任务完成”只说明流程走到了终点,不保证 Writer 的原始内容全部进入安全文档。

已观察到的事实

排查数据库和生成记录后确认:

  • 匹配报告包含 13 条岗位要求;
  • 其中有强匹配和多条弱匹配;
  • Planner 最终只保留了教育模块和两个旧 Evidence ID;
  • 当前默认候选人资料中没有对应教育记录;
  • Writer 的真实模型调用出现结构异常,随后进入确定性降级;
  • 降级结果只能根据 Planner 已选择的内容生成教育条目;
  • 教育记录缺少独立的学位和专业字段,Writer 使用了占位标题;
  • FactValidator 判断占位标题没有证据支持,删除条目;
  • 所有 section 为空后,模板只剩页眉。

这些事实共同说明:匹配成功不等于内容已经通过后续所有合同。

原因分析

直接原因:Planner 丢失了内容覆盖

匹配报告回答的是“岗位要求和哪些证据相关”,Planner 回答的是“最终简历选择哪些证据”。早期实现允许模型自由选择 section 和 Evidence ID,只做了 ID 白名单,没有设置最低覆盖要求。

因此,模型即使看到了多个项目和实习匹配,也可以只返回教育模块。这个响应结构可能完全符合 Schema,却不符合产品目标。

放大因素:降级基线继承了错误计划

Writer 出现结构异常后切换到确定性结果,这个设计本身是正确的。但确定性 Writer 仍然只读取 Planner 允许的 Evidence。

换句话说,降级保证了“不编造”,却没有保证“内容覆盖”。如果输入计划已经过度收缩,安全降级只能稳定地产生一个过度收缩的结果。

最后触发:事实校验删除粒度过大

教育条目使用了没有事实依据的占位标题。早期 FactValidator 遇到标题、组织、日期或副标题中任一字段不受支持时,会删除整个 item。

这会产生级联效果:一个字段错误导致所有安全 bullet 也消失,item 消失后 section 为空,所有 section 为空后只剩页眉。

数据问题:旧记录和默认资料断开

登录功能上线前存在一份未归属的旧候选人资料,其中保存了两条教育经历;注册时邮箱发生过变化,因此系统没有按同邮箱自动接管旧资料。新默认资料包含项目和实习,但教育仍挂在旧记录上。

这不是模型问题,但它暴露了数据迁移和 Evidence 完整性同样会影响生成链路。

排查过程

1. 不先看最终 PDF,逐节点检查持久化结果

推荐顺序是:

  1. 检查 matching 是否包含 Evidence ID;
  2. 检查 plan.sections[*].evidence_ids 是否覆盖预期记录;
  3. 检查 Writer 原始结构和降级元数据;
  4. 检查 fact_validation.blocked_statements
  5. 检查安全 resume_json.sections
  6. 最后检查 HTML 和 PDF。

如果只看 PDF,会把规划遗漏、Schema 异常、事实拦截和模板问题混在一起。

2. 对照候选人子记录和 Evidence

需要同时确认:

  • 业务子记录是否存在;
  • 子记录的 evidence_id 是否存在;
  • Evidence 是否属于当前候选人;
  • Planner 引用的 ID 是否仍然有效。

只检查 Evidence 表或只检查业务表都不够。

3. 区分模型失败和安全降级

日志中记录了 Writer 节点的结构异常类型,但没有保存完整个人 Prompt。确认降级发生后,还需要继续检查降级输入,而不是认为“已经用了本地规则,所以不会有问题”。

最终解决方案

Planner 增加程序级覆盖下限

程序先根据匹配强度和生成开关构造确定性基线:

强匹配 → 中匹配 → 弱匹配 → 其他真实 Evidence

模型仍可调整模块顺序和篇幅,但归一化后要与基线合并:

  • 模型删除整个已启用模块时,补回该模块;
  • 模型只保留同模块中的一条记录时,按排序补回其余候选记录;
  • Evidence ID 不在白名单时仍然删除;
  • 一页和两页分别使用明确的条目与 bullet 上限。

覆盖下限和事实白名单同时存在,前者防止“过度省略”,后者防止“自由补造”。

Writer 合并模型输出和确定性基线

Writer 先尝试把模型响应转换为严格 ResumeDocument。结构不完整时使用确定性文档;结构合法但遗漏整个 section 或条目时,也会把安全基线中的缺失内容合并回来。

这使系统不再依赖模型完整复述所有已选 Evidence。

FactValidator 改为字段级清理

新的策略是:

  1. 分别验证标题、组织、日期和副标题;
  2. 清空不受支持的单个字段并记录拦截原因;
  3. 独立验证每条 bullet;
  4. 只在条目既没有可用标题/组织,也没有安全正文时才删除整个 item。

教育缺少学位和专业时,确定性 Writer 使用学校作为标题,不再生成 education 之类的占位事实。

修复 Evidence 完整性

候选人服务增加 Evidence 修复逻辑:业务子记录存在但 Evidence 丢失时,使用原 evidence_id 优先重建;没有旧 ID 时创建新 ID。修复只恢复当前记录的证据连接,不把无法确认的内容自动标记为人工验证。

对于已确认属于同一人的旧教育记录,本次按同名、同电话和人工上下文确认后复制到当前默认资料,同时保留旧记录,没有执行破坏性迁移。

为什么有些尝试不能解决问题

单纯延长模型超时

延长超时可以减少网络超时,但这次 Planner 已经返回了结构合法但覆盖不足的结果。等待更久不能自动补回被模型省略的 section。

只提高匹配分数

匹配报告本来就不为空。即使把多个弱匹配改成中匹配,如果 Planner 仍能自由删除它们,最终内容仍可能为空。

事实校验全部放行

暂时关闭 FactValidator 会让占位标题进入 PDF,但也会同时放过真正无依据的数字和技术。正确修复是缩小删除粒度,而不是移除事实闸门。

用本地规则替代全部模型

确定性规则适合作为安全下限,但完全替代模型会损失语义匹配和表达优化。问题的重点是建立节点合同,而不是简单选择“模型”或“规则”其中一方。

验证方法

修复后增加了回归测试:

  • 模型计划不能删除整个匹配模块;
  • 同一模块只返回部分 Evidence 时,其余真实候选会按顺序补回;
  • 学位和专业为空时使用学校作为安全标题;
  • 无效字段不会导致有依据的 bullet 一并删除;
  • 技术栈等程序生成标签仍能通过 Evidence 校验;
  • 原始资料没有“提升 30%”时,最终 PDF 不得出现该数字。

当前本地实际结果包含 2 条教育、1 段实习、2 个项目、5 项技能、3 篇论文和 2 项获奖,事实拦截为 0,导出为单页 A4 PDF,正文可以提取。这里记录的是 2026-07-17 的本地验收结果,不代表所有候选人资料都必然适合一页布局。

原理说明

这个问题本质上是多节点工作流中的“合同空洞”:每个节点单独看都可能合法,但组合后没有保证最终目标。

flowchart LR
    M[匹配有内容] --> P[规划过度省略]
    P --> W[Writer 只处理少量输入]
    W --> F[字段错误触发整项删除]
    F --> E[安全文档为空]

Schema 只能保证形状,不自动保证覆盖度、业务价值和跨节点不变量。需要额外定义:

  • 哪些模块不能被无故删除;
  • 至少保留多少真实候选内容;
  • 降级时继承哪一份基线;
  • 校验失败的最小删除单位是什么;
  • 最终文档为空时是否应阻止 completed

后续改进

  • 增加“正文为空”或“版面覆盖过低”的显式业务校验;
  • 使用实际页面高度测量,而不只依赖固定条目数量;
  • 将数据迁移和孤立 Evidence 检查做成可重复审计工具;
  • 在编辑页显示每个模块被省略、补回或拦截的原因;
  • 对导入教育、论文等复合字段提供更明确的人工确认界面。

经验总结

在有界 Agent 中,不能只验证单个节点输出是否符合 JSON Schema,还要验证跨节点不变量。匹配数量、模型成功、任务完成和最终内容完整是四件不同的事。

这次故障最终不是通过“更强的模型”解决,而是通过更明确的程序合同解决:Planner 保证覆盖下限,Writer 保证安全合并,FactValidator 保证精确拦截,数据服务保证 Evidence 连接完整。

相关架构见:从岗位 JD 到可追溯 PDF