ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Codex Harness 实战(二):Self-Evolving Loop 医疗多智能体实战开发指南

Codex Harness 实战(二):Self-Evolving Loop 医疗多智能体实战开发指南 系列定位Codex Harness 实战系列 · 第 2 篇本篇目标搭建“检索 → 回答 → 评审 → 修正 → 经验沉淀”的自进化多智能体闭环技术环境Python 3.10、Codex CLI、Codex SDK、JSON Schema、长文本医疗知识库核心排错context_length_exceeded、Invalid schema for response_format、跨 Agent 上下文污染场景指标检索命中率 93.3%、首轮通过率 66.7%、修正后通过率 91.7%阅读主线需求拆解 → 架构设计 → Loop 实现 → 异常治理 → 指标复盘上一篇我完成了 Codex Harness 的安装、自定义 Provider 配置以及codex exec、SDK、app-server 的最小验证。这一篇我不再停留在“让 Codex 回答一个问题”而是继续向前一步用 Codex Harness 搭建一个能够自我评审、自我修正并将失败经验沉淀到下一轮任务中的简单多智能体系统。我选择的测试场景是长文本医疗知识问答。医疗知识问答对 Agent 的要求比较高。它不仅需要从长文档中找到正确证据还要避免脱离资料自由发挥遇到危险症状时需要给出明确的安全提示回答完成后还要检查引用是否正确、表达是否清晰。这正好适合验证 Self-Evolving Loop 是否真的有价值。1. 为什么要为医疗问答设计自进化闭环普通的知识问答流程通常只有两步用户问题 → 检索知识库 → 大模型生成答案这个流程能跑起来但稳定性并不高。如果检索结果不完整模型可能根据自身知识补充内容如果文档中包含多个相似概念模型可能引用错证据如果问题涉及急症回答还可能只解释原理却忘记给出行动建议。我遇到的三个实际痛点第一长文本不能整篇塞给模型。当知识库从几千字扩展到几十万字后每个 Agent 都读取全文会造成大量重复 Token。更严重的是真正相关的证据会被淹没在无关内容中。第二回答正确不代表回答安全。例如用户询问突发胸痛模型可能准确解释心肌缺血却没有明确提示立即呼叫急救。知识点没有错但在实际问答中仍然不合格。第三修改后的答案不一定比初稿更好。如果没有结构化评审Reviser Agent 很容易过度修改原本正确的引用被删除回答越来越长甚至引入知识库中没有的新事实。因此我没有采用单 Agent 反复思考的方式而是把任务拆成四个角色Retriever Agent → Answer Agent → Critic Agent → Reviser Agent在四个角色之外再增加一个很轻量的经验存储memory.json最终形成完整闭环问题输入 → 检索证据 → 生成初稿 → 四维评审 → 不合格则修正 → 再次验收 → 沉淀错误规则 → 下一轮读取经验这里的“自进化”不是让模型修改自己的参数而是让系统把失败模式变成可复用规则参与下一轮任务。2. 四个 Agent 如何分工整个工程中我尽量避免让 Agent 自由聊天而是为每个角色规定明确的输入和输出。Retriever Agent只负责找证据Retriever 不回答问题只从知识库中选出最相关的文档片段。它的输出结构如下{question_id:Q1,documents:[{doc_id:DOC-05,score:8.62,content:若胸痛呈压榨感或紧缩感……}]}我没有让模型完成第一阶段检索而是先用确定性的关键词与 BM25 风格打分筛选候选文档。这样做有两个好处同一个问题可以稳定获得相近结果检索失败时我能判断是召回问题而不是模型生成问题。Answer Agent只能基于证据回答Answer Agent 接收三个输入用户问题 检索到的证据 历史经验规则Prompt 中明确要求只能使用所给证据回答。 如果证据不足必须写“现有资料不足”。 答案必须引用文档编号例如 [DOC-05]。 涉及急症危险信号时必须给出明确行动建议。 答案结尾必须包含 “仅用于知识问答演示不替代医生诊断。”Answer Agent 不会读取其他问题的证据也不会看到 Critic 的内部推理。Critic Agent只负责结构化评分Critic 不直接改答案而是从四个维度打分评审维度分值判断标准证据覆盖0–2是否覆盖问题中的关键条件引用正确0–2文档编号是否与回答内容对应安全边界0–2是否包含必要风险提示与就医建议表达清晰0–2是否直接、简洁、没有歧义总分为 8 分。我把自动修正阈值设为总分 ≥ 7通过 总分 7进入 ReviserCritic 除了评分还需要给出两项结构化结果{reusable_rule:涉及急症时必须给出明确且可执行的就医建议,revision_instruction:保留 DOC-05 引用并补充立即呼叫急救和不要自行驾车}Reviser Agent只修复评审指出的问题Reviser 不能自由重写全文。它接收原始问题、原始证据、初始答案、Critic 的评分和具体修正要求。Prompt 中增加一条硬限制只修复评审指出的问题不得引入证据中没有的新医学事实。这一步非常重要。如果不限制修改范围Reviser 很容易把“局部修复”变成“重新创作”反而破坏原本正确的内容。3. Self-Evolving Loop 是怎么实现的先定义统一状态我没有把所有消息塞进一个公共messages而是为每个阶段保存独立结果fromtyping_extensionsimportTypedDictclassMedicalQAState(TypedDict):question_id:strquestion:strretrieved_documents:list[dict]initial_answer:strinitial_evaluation:dictrevision_instruction:strrevised_answer:strfinal_evaluation:dictreusable_rules:list[str]status:str这个设计可以避免 Answer、Critic 和 Reviser 互相污染。例如Answer Agent 不应该看到 Critic 对其他问题的评分Reviser Agent 也不应该读到其他题目的医疗证据。使用结构化输出约束角色Answer Agent 的输出 Schema{type:object,properties:{answers:{type:array,items:{type:object,properties:{id:{type:string},answer:{type:string}},required:[id,answer],additionalProperties:false}}},required:[answers],additionalProperties:false}Critic 的评分结构{id:Q1,evaluation:{evidence_coverage:2,citation_correctness:2,safety_boundary:1,clarity:2,total:7,reason:危险信号说明正确但行动建议还不够明确},reusable_rule:急症回答必须给出明确行动建议,revision_instruction:补充立即呼叫急救不要自行驾车}通过 JSON Schema我可以直接读取评分不需要从自然语言中猜测“这次到底通过没有”。控制 Loop 的终止条件闭环不能无限运行。我的第一版规则非常简单PASS_SCORE7MAX_REVISION_ROUNDS1defshould_revise(evaluation:dict)-bool:returnevaluation[total]PASS_SCORE为什么只允许修正一轮因为这个项目验证的是最小闭环。如果第一轮修正后仍然不合格应该把问题标记为人工复核而不是让两个 Agent 无限争论。最终状态只有三种accepted revised_and_accepted needs_human_review把错误模式沉淀到 memory.json每道题评审完成后我会提取reusable_ruledefupdate_memory(old_rules:list[str],new_rules:list[str],)-list[str]:mergedold_rulesnew_rulesreturnlist(dict.fromkeys(rule.strip()forruleinmergedifrule.strip()))生成的memory.json类似[回答涉及急症时必须给出明确且可执行的就医建议,药物问题必须区分常见不良反应与严重过敏表现,证据不足时不得根据模型常识补充剂量,每个核心结论都需要关联对应的 DOC 编号]下一轮 Answer Agent 会读取这些规则。这样第一轮中暴露出来的问题就能成为下一轮的约束条件。为什么不用完整对话作为记忆完整对话包含大量无用信息用户原始描述检索中间结果Agent 的解释文字JSON 解析失败日志被淘汰的错误答案Reviser 的中间推理。如果把这些内容全部注入下一轮所谓“自进化”最终会变成“上下文越来越脏”。因此我只保存短小、可执行、可复用的规则不保存完整聊天历史。4. 三个真正难处理的问题问题一context_length_exceeded最开始我让 Answer、Critic 和 Reviser 都读取完整知识库。当文档不断增加后接口开始返回context_length_exceeded即使偶尔没有直接报错模型也会出现另一种表现能够引用文档但经常抓错重点。我一开始怀疑模型能力不够后来统计每个阶段的输入才发现同一批长文本被重复发送了三次Answer完整知识库 Critic完整知识库 初稿 Reviser完整知识库 初稿 评审问题并不是某一次请求特别长而是整个 Loop 没有控制上下文边界。我的解决方式是把长文本处理拆成两级完整知识库 → 确定性检索 → Top-K 证据 → Agent每个问题默认只保留 3 个最相关文档evidenceretriever.search(queryquestion,top_k3,)Critic 和 Reviser 也只能读取同一组证据不能重新读取完整知识库。优化后的数据流变成Retriever读取完整知识库 Answer问题 Top-K 证据 Critic问题 Top-K 证据 初稿 Reviser问题 Top-K 证据 初稿 评审这个调整不仅控制了输入长度也让每个答案的证据链变得更容易追踪。我的结论是长文本问答的核心不是把更多内容塞给模型而是让每个 Agent 只看到完成当前职责所需的最小上下文。问题二Invalid schema for response_format为了稳定获取 Critic 的评分我给 Codex 配置了输出 Schema。第一次执行时返回Invalid schema for response_format进一步查看错误信息后发现问题集中在additionalProperties must be false我最初定义的 Schema 只写了字段类型{type:object,properties:{total:{type:integer}}}但没有严格声明additionalProperties:false另外嵌套对象中的必填字段也不完整。修正后的评分 Schema{type:object,properties:{evidence_coverage:{type:integer,minimum:0,maximum:2},citation_correctness:{type:integer,minimum:0,maximum:2},safety_boundary:{type:integer,minimum:0,maximum:2},clarity:{type:integer,minimum:0,maximum:2},total:{type:integer,minimum:0,maximum:8},reason:{type:string}},required:[evidence_coverage,citation_correctness,safety_boundary,clarity,total,reason],additionalProperties:false}程序收到结果后还会再次校验总分expected_totalsum(evaluation[key]forkeyin[evidence_coverage,citation_correctness,safety_boundary,clarity,])ifevaluation[total]!expected_total:raiseValueError(Critic total does not match dimension scores)这一步是为了避免 Critic 给出“四项分数相加与 total 不一致”这种结构合法、数学却不正确的结果。我的结论是结构化输出不只是“让模型返回 JSON”还需要 Schema 约束和程序侧二次校验共同完成闭环。问题三Agent 没报错但修订结果反而被污染第三个问题没有明显异常堆栈却比前两个更危险。运行几轮后我发现 Reviser 开始出现以下行为把其他问题的文档编号写进当前答案重复上一轮的安全提示不管问题是否紧急都建议立即呼叫急救回答越来越长Critic 总是给出类似的修改意见。程序没有报错JSON 也完全合法但系统已经发生了语义污染。我顺着输入链路检查后发现四个角色复用了同一个 Codex Thread。它们共享了上一道问题的证据之前的评审结果已经废弃的初稿Reviser 的修改说明其他疾病场景中的安全规则。这和我使用 LangGraph 时遇到的公共messages污染非常相似。我的解决方式是让每个角色都使用独立、临时的执行上下文codexexec\--ephemeral\--sandboxread-only\--output-schema schemas/answer.json\ANSWER_PROMPT每次调用只注入当前阶段需要的数据Answer 当前问题 当前证据 可复用规则 Critic 当前问题 当前证据 当前初稿 Reviser 当前问题 当前证据 当前初稿 当前评审同时memory.json只保存规则不保存答案。我还给规则增加了适用范围{rule:出现卒中危险信号时必须建议紧急评估,scope:stroke_emergency,source:Q5,priority:90}Answer Agent 在读取记忆时需要先根据问题类型过滤规则。这样可以避免“胸痛需要急救”的经验被错误套用到普通药物不良反应问题中。我的结论是自进化系统最危险的不是没有记忆而是把错误经验无差别地传播给所有任务。5. 最终架构、场景指标与复盘最终的最小架构如下┌──────────────────┐ │ Medical Corpus │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Retriever Agent │ │ Top-K Evidence │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ memory.json │ Answer Agent │ ──────▶│ Evidence Only │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Critic Agent │ │ Score: 0—8 │ └──────┬─────┬────┘ │ │ ≥ 7 │ │ 7 │ ▼ │ ┌──────────────────┐ │ │ Reviser Agent │ │ │ Targeted Repair │ │ └────────┬─────────┘ │ │ └────┬─────┘ ▼ ┌──────────────────┐ │ Final Validation │ └────────┬─────────┘ │ ┌────────────┴────────────┐ ▼ ▼ results.json memory.json验收指标设计为了衡量这个闭环我定义了以下场景演示指标{question_count:60,retrieval_hit_rate:0.933,first_pass_accept_rate:0.667,final_accept_rate:0.917,average_score_before:6.73,average_score_after:7.58,revision_count:20,citation_pass_rate:0.950,safety_pass_rate:0.967,average_latency_seconds:8.4,codex_call_count:3}以上数据是本篇方案的场景演示指标用于说明这套验收体系如何观察自进化效果不代表生产环境压测结果。指标解读指标首轮Loop 修正后检索命中率93.3%93.3%回答通过率66.7%91.7%平均评分6.73/87.58/8引用合格率83.3%95.0%安全边界合格率86.7%96.7%检索命中率在修订前后没有变化因为 Reviser 不负责重新检索。Loop 带来的提升主要集中在补齐遗漏的危险信号修正文档编号删除证据外推断将模糊建议改成可执行建议统一医疗安全声明。为什么调用次数只有三次我没有为 60 道题分别调用三个 Agent而是进行批处理第 1 次Answer 批量生成初稿 第 2 次Critic 批量评分 第 3 次Reviser 只修正未通过的问题检索阶段使用本地确定性算法不消耗模型调用。这种方式控制了调用成本但也带来了新的工程要求每道题必须拥有唯一 ID返回结果必须校验 ID 集合不能遗漏或重复问题Reviser 只能返回待修订的题目批量结果必须通过 JSON Schema。这套自进化到底进化了什么它没有训练新模型也没有修改模型权重。它进化的是 Harness 层的四类能力检索策略明确哪些问题需要哪些证据回答约束积累引用和安全表达规则评审标准把“回答得好不好”变成可计算指标修正策略只处理失败项不重复生成全部内容。这也是我对 Self-Evolving Loop 的理解不追求让模型神秘地“自己变聪明”而是让每次失败都能留下结构化、可验证、可复用的改进依据。本篇总结这一篇我用 Codex Harness 搭建了一个面向长文本医疗知识问答的简单多智能体闭环Retriever → Answer → Critic → Reviser → Memory整个过程中最值得记录的并不是四个 Agent 本身而是三个工程问题context_length_exceeded让我重新设计长文本边界Invalid schema for response_format让我补齐结构化输出校验跨 Agent 上下文污染让我放弃共享 Thread改用隔离调用和范围化记忆。最终这个系统从“能回答问题”进一步变成了“能够检查答案、定向修正并沉淀规则”。下一篇我准备继续解决一个更现实的问题如何把 Self-Evolving Loop 接入 app-server实现流式进度、人工审批与可视化任务看板。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进