把大模型生成限制在事实边界内:Evidence 驱动的内容生成与校验
一句话理解
Evidence 驱动生成不是要求模型“尽量诚实”,而是让每条输出都携带可检查的证据引用,并由普通程序决定它是否有资格进入最终结果。
本文来自 ResumeTailor Agent 的实际实现。场景虽然是岗位定制简历,但方法同样适用于申请材料、项目报告、研究总结和其他不允许自由编造事实的生成任务。
为什么只写 Prompt 不够
一个常见 Prompt 会要求模型:不要虚构经历,不要添加不存在的数据,只根据用户材料回答。这种要求有价值,但不能成为唯一防线。
原因包括:
- 模型可能把 JD 中的技能误当成候选人的技能;
- 模型可能为了让表达更有说服力而补充百分比;
- 长上下文中,模型可能错误关联不同经历;
- 结构化响应异常后,业务代码可能错误采用部分字段;
- 用户在编辑页手工加入新事实,也可能绕过最初 Prompt。
因此,真实性需要成为数据模型和导出流程的一部分,而不是一句自然语言提醒。
核心概念
EvidenceItem
每条原始经历在进入生成流程前转换成 EvidenceItem,至少包含:
- 稳定的 Evidence ID;
- 来源类型和原记录 ID;
- 原始文本;
- 标准化文本;
- 是否经过人工验证;
- 所属候选人。
原始文本用于追溯,标准化文本用于搜索、匹配和规则检查。两者不能互相替代:只保存标准化内容会丢失原始语境,只保存原文又不利于结构化处理。
带引用的生成内容
简历条目和 bullet 不是纯字符串,而是带证据和岗位要求引用的对象:
class ResumeBullet(BaseModel):
text: str
evidence_ids: list[str] = Field(min_length=1)
jd_requirement_ids: list[str] = Field(default_factory=list)
validation_status: Literal[
"supported",
"partially_supported",
"unsupported",
"needs_confirmation",
]
这里最重要的字段不是 text,而是 evidence_ids。没有证据引用的正文不能被视为正常输出。
白名单而不是自由引用
模型只能看到当前节点允许使用的 Evidence,并且返回的 ID 必须位于输入白名单。模型生成一个格式正确但不存在的 UUID 时,程序会删除该引用;引用删除后没有证据的内容也会被拦截。
这种设计把问题从“模型是否可信”转化成“输出引用是否属于已知集合”,后者可以确定性判断。
工作原理
flowchart LR
RAW[原始资料] --> NORMALIZE[标准化并创建 Evidence]
NORMALIZE --> MATCH[JD 与 Evidence 匹配]
MATCH --> PLAN[选择允许使用的 Evidence]
PLAN --> WRITE[生成带 Evidence ID 的内容]
WRITE --> ID[Evidence ID 白名单]
ID --> FACT[数字/术语/语义校验]
FACT --> SAFE[安全 ResumeDocument]
SAFE --> TEMPLATE[确定性 HTML/PDF]
模型参与匹配、规划和写作,但从 Writer 到 PDF 之间存在程序级事实闸门。最终模板只接收安全文档,而不是 Writer 的原始响应。
确定性校验检查什么
Evidence ID
item 和 bullet 引用的 ID 必须存在,并属于当前候选人。跨候选人引用即使在数据库中存在,也不能成为当前简历的证据。
数字和百分比
数字是最容易造成现实误导的内容。系统会提取陈述中的百分比、排名、GPA、DOI、证书号等数值,并检查支持证据中是否存在相同事实。
例如,原始资料只有:
优化了接口响应速度。
生成内容却变成:
优化缓存策略,使接口响应速度提升 30%。
即使前半句可能合理,“30%”在证据中不存在,因此整条陈述不能直接导出。
技术专有词
如果生成内容新增了证据中没有出现的框架、数据库或模型名称,也可能意味着模型把 JD 需求反向写入了候选人经历。校验器会比较英文技术词和标准化证据,缺失的专有词提高风险等级。
语义覆盖
纯字符串相等无法覆盖正常改写,因此系统还使用轻量语义重合规则判断陈述和 Evidence 是否存在足够关联。这个规则只适合做安全下限,不能被解释为精确的自然语言蕴含证明。
字段级清理
早期实现只要条目标题、组织或日期中有一个字段不受支持,就删除整个条目。实践证明,这会把同一条记录中仍然有依据的 bullet 一并丢掉。
后来的做法是:先清理不受支持的字段,再判断条目是否仍有标题、组织或安全正文。事实校验应该尽量精确到字段和陈述,而不是使用过大的删除粒度。
四种验证状态
| 状态 | 含义 | 导出策略 |
|---|---|---|
supported |
证据能够支持陈述 | 可以导出 |
partially_supported |
部分表达有依据,但仍有缺口 | 当前实现按风险处理,不直接当作完全安全 |
unsupported |
存在明确无依据内容 | 必须删除 |
needs_confirmation |
语义关系不足或来源尚未确认 | 用户确认前不导出 |
这些状态不是模型的自评分数,而是业务流程状态。尤其不能因为模型返回 supported 就跳过程序校验。
用户编辑同样要经过闸门
结构化编辑器允许用户修改标题、正文和模块顺序,但修改后的内容不会直接覆盖 PDF。保存后,任务重新进入 validating,再次执行 Evidence 和事实检查,然后重新渲染。
这条规则避免出现一个常见漏洞:AI 输出受限,但人工编辑接口可以任意加入新数字并立即导出。安全边界应位于“所有内容进入 PDF 之前”,而不是只位于“模型调用之后”。
一个重要回归测试
项目中保留了一个专门的回归测试:
- Evidence 中不包含“提升 30%”;
- 构造包含“提升 30%”的结构化 bullet;
- 执行
ResumeFactValidator; - 断言该陈述为
unsupported; - 断言安全文档、HTML 和最终 PDF 均不包含这句话。
这个测试比只断言校验报告更重要,因为真正的安全目标是“内容没有进入最终交付物”。
常见误区
用第二个模型代替事实校验
Critic 可以发现过度包装和表达问题,但它仍然是概率模型。第二次模型调用可以补充审查视角,不能替代 ID、数字和权限检查。
把 JD 当成候选人证据
JD 只能证明岗位要求某项技能,不能证明候选人拥有该技能。匹配流程必须把“需求来源”和“候选人来源”分开建模。
为了填满页面放宽事实标准
页面不足时,可以扩大真实经历覆盖范围、减少省略或调整 CSS,但不能添加无证据成果。版面完整性和事实真实性不是同一级约束,后者优先级更高。
认为 verified=false 就完全不可用
未人工确认的导入结果需要提示风险,但是否允许进入草稿、是否允许进入最终 PDF,仍应由明确的产品策略决定。不能让一个布尔字段承担全部语义。
适用边界
确定性规则也有局限:
- 高质量改写可能和原文词面重合很低;
- OCR 错误会污染 Evidence;
- 同一个数字在不同上下文中出现,不能仅凭存在性证明对应关系;
- 作者位次、论文状态等复合字段需要更细的结构化数据;
- Evidence 本身来自用户输入,不等于已通过外部真实性核验。
因此,这套机制解决的是“生成内容是否有输入依据”,不是替代背调、论文检索或证书验证。
实际应用
Evidence 驱动模式可以迁移到:
- 根据会议记录生成决策纪要;
- 根据合同条款生成风险摘要;
- 根据实验记录生成研究报告;
- 根据工单和日志生成故障复盘;
- 根据产品文档生成客服回答。
共同点是:输出必须能够回答“这句话从哪里来”。
总结
控制大模型幻觉不能只依靠 Prompt。更可靠的做法是让 Evidence 成为贯穿数据、匹配、写作、编辑和导出的主键,再用确定性程序执行白名单、数字、术语和权限检查。
模型负责理解和表达,程序负责决定哪些内容可以成为事实。这条边界是可信生成系统最值得复用的设计。
项目整体实现见:从岗位 JD 到可追溯 PDF。