1. 项目缘起:科研场景为什么需要独立设计 Agent Harness
做科研类 Agent 的人和做通用 Agent 的人,对"可靠"两个字的理解是完全不同的。通用 Agent 聊个天、查个地图、订个外卖,答错一个环节用户大概率只是觉得"这产品有点笨"。但科研 Agent 一旦在文献结论、数据引用、实验参数上出了错,轻则浪费几天实验周期,重则整篇论文的立论基础都得推翻。ScienceBuddy 这个项目从一开始就不想做"什么都能干一点的助手",而是想做一个在科研流程里真正能扛事、出错了能自己发现并修正的 Agent Harness——也就是承载 Agent 思维与执行的那套骨架系统。
我最初调研过一圈现成的 Agent 框架,比如 LangGraph、AutoGen 这类通用编排方案。它们的问题倒不是不能跑,而是太"顺滑"了:默认假设模型输出基本可靠、工具调用一次就成功、中间环节出了错可以低成本重试。但科研场景恰恰相反,模型输出大概率不可靠,工具调用往往要跨多个数据源,重试的成本也不是几秒钟,而是可能牵扯到污染实验环境的风险。ScienceBuddy 在架构上的核心选择就是放弃"顺滑",主动引入一套双层递归自进化机制,用结构化的方式管理不确定性,而不是靠运气。
先说清楚这套 Harness 到底在管什么。它管的是 Agent 的完整生命周期:目标拆解、规划行动、工具调用、结果校验、错误恢复、经验沉淀。普通框架把这几件事揉在一个循环里,看起来简单,实际上模型一烧就乱。ScienceBuddy 的思路是把每个环节都做成独立的受控组件,然后用一套递归机制让 Agent 在任务内部和任务之间不断自我修正。英文里管这种设计叫 Harness Engineering,说白了就是把"让模型发挥能力"这件玄学的事,变成一套可以测试、可以监控、可以迭代的工程系统。
这套系统适合谁去参考?如果你正在做科研自动化、文献综述工具、实验辅助 Agent,或者你的 Agent 需要对接大量高风险的业务工具,科学领域只是其中一个场景,那 ScienceBuddy 的很多设计思路是可以直接平移过去的。代码层面我不打算贴完整实现,那个太长,而且项目在持续迭代。我更想讲清楚的是里面那些"不写下来就容易被忽略"的设计决策和踩坑经验。
2. 双层递归自进化:两个层面的"自己改自己"
2.1 第一层递归:任务执行中的行动计划自修正
ScienceBuddy 的第一层递归发生在单个科研任务的执行过程中。比如让 Agent"调研一下 2023 年以来钙钛矿太阳能电池稳定性方面的主要进展",这在通用框架里可能就是一个大 prompt 加几次工具调用。但 ScienceBuddy 会把任务层层拆成子任务,每个子任务有自己的行动计划,这个行动计划本身还有一个递归修正循环。
具体机制是这样的:Agent 每完成一个子任务,不会直接进入下一个,而是先过一遍"成果校验器"。这个校验器会对当前结果提三个问题:这个结论有没有直接引用来源?引用的来源是否经过权威性过滤?结论与已知常识是否存在冲突?如果三个问题里任何一个不过关,Harness 就会把校验结果和失败原因打包,重新喂回给规划器,让规划器调整接下来行动计划。
这里有一个容易被忽略的关键点,就是"返工"的姿态。很多 Agent 框架一遇到校验失败就直接重新生成整个答案,导致模型陷入反复横跳,甚至把原本正确的中间结论也改坏了。ScienceBuddy 的做法是只允许修正后续的行动计划和当前子任务的局部输出,冻结已经通过校验的前序结果。这个设计像极了科研论文的审稿流程:审稿人提意见,作者只修改被质疑的部分,不会因为某个小问题就把整篇文章重写。
实现上,这个冻结机制需要一个结构化的状态对象来承载任务进度。我用的是一个逐层嵌套的字典结构,每个子任务有 status、artifact、validation_report 三个字段,只有 status 为 passed 的子任务结果才有资格被后续步骤引用。这样规划器在生产下一步计划时,上下文里只会出现经过校验的事实,不会把原始模型输出直接铺进去。如果读者在复刻这套机制时跳过这个状态管理细节,只是用对话历史的自然累积,那递归自修正执行到第三四轮的时候,上下文里就会被各种失败信息污染,模型会越来越糊涂。
2.2 第二层递归:跨任务的技能与策略进化
第一层递归解决的是"当前任务怎么完成得更好",第二层递归解决的是"下一次类似任务怎么从第一天就做得更好"。这是 ScienceBuddy 区别于大多数 Agent 项目的地方,也是"自进化"三个字真正落地的位置。
第二层的实现机制是经验仓库。每当一个任务整体完成,ScienceBuddy 会把整个执行轨迹压缩成一条结构化经验记录,包含任务类型、规划策略、踩过的坑、最终有效的解法路径、以及无效方法的失败特征。这些经验会被周期性离线聚合成"策略模型",在下一次处理同类任务时,规划器会优先参考这些策略,而不是从零开始推理。
举个例子,假设某个 Agent 在处理"比较两种催化剂性能"类任务时,多次出现同一个问题:第一次工具调用时只检索了英文库,漏了中文研究的重要数据,导致结论偏差。第一层递归会在这次任务内部修正方向,但修正过程很痛苦,等于先犯错再纠错。第二层递归就聪明在这里:经验仓库里记下了"比较类科研任务必须先做语言覆盖检查"这条策略,下一次同类任务还没开始规划,策略提示就已经注入了初始行动计划里,错误在发生之前就被规避掉了。
这里要注意"策略注入"和"盲目套模板"之间的分寸。我把策略分成两类:硬性策略和软性参考。硬性策略是高置信度的避坑规则,必须注入;软性参考是历史方案的低置信度相似推荐,只作为候选提示。这种区分避免了经验仓库变成教条库,不同科研子领域的差异很大,强行套用相似任务的经验有时反而适得其反。
两层的名字里都有"递归",但含义略有不同。第一层是闭环修正,执行到一半发现问题就回到规划环节重新规划;第二层是累积进化,一个任务结束后的经验沉淀会影响未来所有相似任务的起点。两层嵌套起来,就形成了"当前任务自修正 + 长期能力自进化"的双轨结构。我认为这是科研 Agent 可靠性设计里比较值得复刻的一种模式。
3. Harness 层的关键设计:边界、沙箱与确定性
3.1 工具调用的边界治理
科研 Agent 的工具调用和普通 Agent 有一个本质区别:科研工具往往有毒副作用。我说的不是安全意义上的毒,而是状态污染的毒。你调用一个数据库查询接口,它会改写本地缓存;你启动一个计算容器,它会在宿主机留下进程和临时文件。如果 Agent 的执行是不可回溯的,那么一次失败的实验模拟可能污染后续所有数据。
ScienceBuddy 的 Harness 层因此引入了一个非常朴素但有效的设计:所有工具调用都在独立沙箱里执行,沙箱与主流程之间只通过结构化数据通信。实际实现中,简单工具走的是子进程隔离,复杂工具走的是轻量容器。每次工具调用的输入输出都有完整记录,失败时可以随时从最后一个可靠状态回滚。
这里我想强调一点:沙箱的重点不是防恶意代码,而是防半成品的状态泄漏。即使你的工具完全可信,执行过程中产生的中间态也可能带偏后续判断。我有一个印象很深的调试经历:Agent 在查询某个蛋白质数据库时,工具内部默默加载了一份过期配置,导致后续所有序列比对结果都朝着一个细微但方向性偏差的角度偏移。这个偏差单看每一步都不明显,但因为 Harness 每次都是从主流程的信任边界重新拉取环境,沙箱重建后问题立刻消失,排查才得以收敛。
3.2 确定性优先的编排循环
Agent Harness 里的一个反直觉设计是:规划的入口必须确定性。很多人觉得 Agent 的强项就是随机性、创造性、发散性,所以编排环节也要尽量让模型自由发挥。但 ScienceBuddy 在编排循环上反其道而行,主循环是一个完全确定性的有限状态机,只在状态机的特定出口挂载 LLM 决策点。
我打个比喻,这就像自动驾驶:方向盘、油门、刹车的物理控制逻辑是完全确定性的,AI 只在高层的路径规划决策里介入。如果 AI 直接接管方向盘,任何一个感知错误都可能让车飞出路面。ScienceBuddy 的主循环定义了 INIT → PLAN → EXECUTE → VALIDATE → REVISE → COMPLETE 这几个固定状态,模型只负责在每个状态里产出该产出的内容,状态间的转换规则是硬编码的。这个设计让整个 Harness 是可测试的。线上任何一个环节出了问题,日志里能精确定位到是转移到哪个状态时失败,而不是一头雾水地重放整个 prompt 链。
这种确定性和灵活性之间的平衡,恰好也是 Harness Engineering 和单纯"套一个大 prompt"的核心区别。前者是可以工程化调优的,后者只能靠玄学式地调整措辞。我在 ScienceBuddy 上做的几乎每一次可靠性优化,都是围着这座确定性的骨架做加法,比如增加校验器数量、细化失败分类、在状态转换处加条件守卫。骨架本身几乎没动过,但系统的可靠性上限被持续抬高。
3.3 失败恢复的幂等设计
科研任务动辄跑几十分钟甚至几小时,中间任何一次工具调用失败如果从头重跑,成本完全不可接受。ScienceBuddy 在失败恢复上做了幂等化设计。每个工具调用都有一个任务内唯一的 call_id,工具的输出会以 call_id 为键缓存。哪怕失败后重试,只要输入参数相同且上一次的部分副作用已经被沙箱隔离清理,系统可以直接复用缓存的输出,不用重新执行。
这个设计听起来简单,落地时有一个容易被忽略的坑:Token 生成式模型的输出天然带有随机性,即使同一 prompt 重试两次,结果也可能不同。所以缓存键不能只包含输入参数,还得显式约定当前任务的确定性种子。ScienceBuddy 对规划器的采样温度做了分段设计:探索阶段允许稍高的温度让方案多样,执行阶段的校验和重试统一用接近 0 的温度,保证缓存命中逻辑不会被模型随机性搅乱。
4. 核心模块的实操实现与关键参数
4.1 规划器与校验器的协作方式
规划器是 ScienceBuddy 里最耗 Token 的模块,因为每层递归都会唤起规划器。为了控制成本,我没有用"一整个大规划"的模式,而是让规划器只产出当前步的最优动作,以及一个轻量的长期路线图。长期路线图由一层较弱的模型生成,当前步的最优动作则由强模型重点保障。这样既保证了方向上有规划,又避免每次规划都消耗满格能力。
规划器的输出是一个 JSON 结构,包含三个字段:action_type、target、rationale。rationale 字段里要求模型写出选择这个动作的理由,不是为了给用户看,而是为了后续校验错误时对齐"模型意图"和"实际结果"。比如模型说"选择 Python 代码执行工具来计算统计显著性",结果代码跑出来一个异常值,校验器把 rationale 提取出来做归因,可以快速判断是代码写错、数据源错、还是统计方法本身选错。这个对齐设计让递归修正的路径短了很多,归因准确率从大约 60% 提升到了 85% 以上。
校验器这边,我维护了一个预定义检查器列表,每个检查器只做一件事。文档引用检查器验证引用格式和 DOI 有效性;数值一致性检查器对比多个结论里引用的同一数据是否自洽;逻辑连贯性检查器评估结论链是否有跳跃。这些检查器全部是确定性代码,不依赖模型判断,因为它们本身就是简单规则。真正的模型级校验只在所有确定性检查器通过后才会触发,此时让模型充当"怀疑的审稿人",从全局视角寻找隐藏矛盾。
4.2 经验仓库的聚合与注入策略
经验仓库是第二层递归的物理载体,它的数据结构和聚合频率值得展开讲。仓库里每一条经验记录我设计成五个部分:task_signature、plan_signature、failure_trace、solution_trace、lessons。task_signature 是对任务描述做语义哈希得到的聚类标识,同一类任务会聚在一起。plan_signature 记录的是初始计划的模板指纹,用于判断"是否照着经验做了但结果仍然失败"的情况。failure_trace 和 solution_trace 分别记录失败路径和最终有效路径,lessons 是从两者对比中萃取的自然语言规则。
聚合过程是离线的,我不会让每次任务结束都立即影响策略库,因为单个任务的样本方差太大。实际操作是:每完成 20 个相似任务,触发一次策略聚合,把这一批任务的 lessons 做一致性比对,只有被重复验证过至少三次的经验才能晋级为硬性策略。这个门槛过滤掉了很多偶然因素。有一次 Agent 在一个数据源特别干净的查询里成功了,经验聚合差点把"跳过重复数据清洗"写成策略,幸好晋级门槛设了三次验证的规则,这条偶然经验最终未能入库,避免了后续在脏数据场景中误导规划器。
注入策略方面,硬性策略会在每次规划前注入到规划器 prompt 的开头作为约束,软性参考则以可选附注的形式放在 prompt 末尾。我同时做了一个小技巧:策略注入时会标注这条策略的验证次数和最近一次生效时间,当 Agent 解决不了问题时,它可以在 REVISE 阶段主动质疑策略的适用性——这个机制防止了经验仓库变成一种新型的"prompt 固化"。
4.3 工具注册与动态加载机制
Harness 最容易被做烂的地方是工具注册。有些人直接维护一个巨大的工具列表,一次性把所有工具的描述、参数 schema 塞给模型,结果模型在大列表里挑花了眼,经常选错工具。ScienceBuddy 采用的是分层的工具路由:Harness 层维护一个粗粒度的工具目录,按科研场景分类;每次规划时,先在目录上做一次基于关键词的粗筛,把候选工具从几十个收敛到三五个,再把筛选后的工具描述注入给模型。
这个粗筛器是规则加小模型的混合体,规则占七成,小模型占三成。规则部分处理典型的命名匹配和参数类型匹配,小模型部分处理模糊的"用户意图到工具"映射。这样做的效果很直接:工具选择准确率从 68% 提升到 91%,模型的决策方差明显降低。我见过很多 Agent 项目的失败不是能力问题,而是工具太多了导致选择困难,这个分层路由思路算是性价比很高的解法。
动态加载机制我还特意做了一个版本化设计。每个工具注册时带一个 schema_version,Harness 在注入工具描述时会检查模型输出中的工具调用是否遵循了当前版本。如果遇到版本不匹配,不是简单重试,而是刷新工具描述后让模型重新理解一次。这个机制源于我踩过的一个坑:工具更新 schema 后旧版缓存没清,Agent 按旧参数调用接口,报错信息又不够直观,整整排查了两个小时才发现是版本错位。
5. 构建过程中的关键取舍与踩坑实录
5.1 递归深度失控时的熔断机制
递归自进化的一个天然风险是:递归可能陷入循环。第一层递归在任务内部修正时,如果校验器连续多次返回同样类型的失败,规划器理论上会一直尝试修改计划,消耗更多 Token,产出却越来越贫瘠。ScienceBuddy 写了一个三级熔断机制。第一级是次数熔断,连续三次同类失败就自动停止当前子任务的执行,转入手动归因模式;第二级是资源熔断,统计每次修正消耗的平均 Token 数,一旦超过预算的 150% 就触发电子围栏;第三级是路径熔断,跟踪规划器每次修正后行动计划的差异度,如果差异度在两次修正之间小于一个阈值(比如 2%),说明模型已经在自己纠正自己了,这时候也立刻熔断。
熔断机制上线之前,我跑过一次压测,让 Agent 处理一个带有矛盾文献的任务,结果系统在递归修正循环里转了十七轮,Token 消耗了常规任务的三十倍,最后产出的结论居然是从第三轮那次修正版本倒退回初始版本的。这个案例深刻地说明一个道理:自进化的前提是有明确的停止条件。没有熔断的自进化不是智能,是灾难。
5.2 Prompt 设计与上下文预算管理
科研 Agent 的上下文管理比通用 Agent 复杂得多,因为一个任务过程中会产生大量引用片段、工具输出、校验报告。如果一股脑塞进对话历史,Context Window 很快就会被撑爆。ScienceBuddy 的做法是上下文分池管理:核心池保留当前计划、当前子任务输入输出、最近一轮校验报告;知识池存放检索到的文献摘要和工具长输出,但只有被显式引用的部分才会进核心池;策略池存放经验仓库注入的策略,只在关键节点参与。
三个池子之间通过一个引用机制联动。模型输出的任何论断如果引用了知识池里的某个条目,必须在输出里带上 pool:knowledge:{id} 的引用标识。校验器会校验这些引用的真实性,确保模型没有张冠李戴。我在实验中发现,这个显式引用机制对幻觉的抑制作用比任何提示词技巧都有效,因为它在生成阶段就逼着模型做了一次"来源核对"的心理动作。
CPU 和 GPU 成本方面,分池管理还有一个额外收益:可以针对不同池子做缓存,知识池的检索结果在任务里去重,同一个文献摘要不需要为每个子任务重新注入一遍。实际跑下来,典型任务的平均 Token 消耗降低了约四成,而结论质量没有下降。
5.3 高可靠环境的日志与可观测性
最后聊聊可观测性。Agent 系统最让人头疼的就是"过程不可重现",同一个 prompt 在同一个模型上两次输出可能完全不同。ScienceBuddy 在日志系统上做了一个全量追踪的设计,每次规划、每次工具调用、每次校验判定,都会记录一份 trace 文件,包含当时的完整上下文快照、模型参数、随机种子、工具响应耗时。这个 trace 文件是调试的核心凭据。
写到这里我想分享一个建议:给 AI Agent 项目做日志,不要只记错误,要记成功。成功路径的 trace 同样重要,因为它能让你后期训练校准器和优化策略时,有正向的参照样本。我在完善第二层经验聚合时,最大的瓶颈恰恰是缺少高质量的成功案例。所谓自进化,不只是从失败中学习,更是从成功中提炼可复制的模式。少了成功样本的聚合,系统只学会躲坑,没学会提效。
6. 从 ScienceBuddy 看科研 Agent 的可靠性上限
对我来说,ScienceBuddy 这个项目的价值不在某一个具体的模块,而在于它提供了一条清晰的路径:通过结构化架构来驯服大模型的随机性。第一层递归让单次任务具备纠错能力,第二层递归让系统具备跨任务的学习能力,Harness 层的确定性骨架为每一层提供了可观测、可控制的轨道。三层协同下来,科研 Agent 从"偶尔好用"的概率性工具,变成了"稳定可用"的可靠工程系统。
当然,这套架构并不是银弹。它在需要高度创造性发散的任务里会显得笨重,确定性的状态机也会拖慢一些本可以极速完成的小任务的响应速度。但这个权衡在科研场景里是完全值得的,因为科研用户最看重的从来不是花哨,而是每一步结论都经得起推敲。ScienceBuddy 的设计本质上是在用工程复杂度换取认知可靠性,这个方向我认为是对的,也值得更多做 Agent 基础设施的人沿着这个思路继续探索。
如果你手头也在做科研自动化的 Agent,或者正被通用 Agent 框架的不可控弄得头大,我建议先不要急着加更多模型能力,而是回头审视一下你的 Harness 骨架:状态流是否明确、失败恢复是否幂等、上下文管理是否有结构化边界、经验沉淀是否被纳入系统设计。这四个问题想清楚了,可靠性自然上一个台阶。科学本身追求的就是确定性,那帮助科学家干活的 Agent,也应该在确定性上多花心思。