匹配不少,为什么最终简历仍然是空的:一次多节点生成链路复盘
问题现象
在一次岗位简历生成中,匹配分析页已经显示多条强、中、弱匹配,部分岗位要求还关联了多个 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,逐节点检查持久化结果
推荐顺序是:
- 检查
matching是否包含 Evidence ID; - 检查
plan.sections[*].evidence_ids是否覆盖预期记录; - 检查 Writer 原始结构和降级元数据;
- 检查
fact_validation.blocked_statements; - 检查安全
resume_json.sections; - 最后检查 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 改为字段级清理
新的策略是:
- 分别验证标题、组织、日期和副标题;
- 清空不受支持的单个字段并记录拦截原因;
- 独立验证每条 bullet;
- 只在条目既没有可用标题/组织,也没有安全正文时才删除整个 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。