能算的绝不调模型:resolve-harness 的确定性 Fast Path 运行时

作者:

🇬🇧 English

resolve-harness 是一个刻意保持"可拆解、可替换"的极简 Agent Harness:LangGraph 工具循环、LiteLLM 多模型路由、分层记忆、沙箱化工具,外层套一套 Planner → Specialist → Evaluator → Reporter 的多智能体任务编排(PSE)。它的核心主张是"能算的绝不调模型"——在 Specialist 执行层前加了一道三层 Fast Path:内置匹配器零模型直出;未命中时让模型自写检测器,经 AST 白名单沙箱验证后持久化复用;稳定者由人工晋升进源码库。

本文从 harness 全景讲到 PSE 编排的真实形态,再用端到端走查和可落地的玩法示例说明:这套骨架最值钱的不是某个具体功能,而是它能让你把多少自己的确定性逻辑和领域工作流,"长"成专属的 Agent。

为什么"能算的绝不调模型"

大多数"任务"其实是确定性的:算术运算、进制转换、闰年判断、日期推算、单位换算。让 LLM 反复处理它们有三个代价:

  • :一次模型调用以秒计,一段 Python 以毫秒计;
  • :每次都要付 token 成本;
  • 不稳:同一个问题今天答对、明天答错。

于是设计原则很简单:先问代码能不能算,再问模型。命中确定性路径的查询全程零模型调用——而且整个过程对 UI、记忆、编排循环完全透明,任务树照常渲染,只是多了一个 zero-model 标注。

但这一节的真正用意不是省那点 token。它把系统清楚地切成两层:可控的确定性(交给代码)与可控之外的智能(交给模型)。切得越干净,你就越敢把想象力往上堆——因为"失控"的那部分被牢牢关在模型调用里,而"可靠"的那部分可以无限复制、加速、复用。后面所有的玩法,都建立在这个切分上。

Harness 全景:四根支柱

README 里有一张自我定位的表:这个项目的目标不是"又一个框架",而是把 agent 的关键机制拆开讲清楚、每一块都可替换

支柱 职责
Loop LangGraph StateGraph:agent → tools → agent… 循环,max_steps 防失控(src/resolve_harness/graph/
Harness 单一装配点:配置、模型路由、工具注册表、记忆、循环(src/resolve_harness/harness.py
Memory 短期(会话转录)+ 长期(SQLite 事实,经工具读写)(src/resolve_harness/memory/
Fast Path 确定性查询纯代码直出;未命中则生成检测器持久化复用(src/resolve_harness/fastpath.py · codegen.py

模型路由基于 LiteLLM:一份 openai/<model> 字符串通吃各家 provider,还支持按角色覆盖——Planner 用便宜模型、Specialist 用强模型,各自独立配置。

这四根支柱之所以重要,是因为它们把"智能"和"基建"解耦了。你想换模型?改一行路由。想加记忆?动 Memory 支柱。想塞新能力?往工具注册表挂。Fast Path 只是其中一根——但它是让整套 harness 从"能聊"变成"能干活"的那根。

PSE 编排:四个角色把目标变成交付物

聊天模式之外,任务模式是一套多智能体流水线:

  • Planner:把一句目标拆解成带顺序的子任务列表(结构化 JSON);
  • Specialist:每个子任务交给一个专家 Agent,进入各自的 agent → tools → agent 工具循环(读写沙箱文件、fetch、跑脚本),支持并行 fan-out;
  • Evaluator:对照目标验收每个结果,返回 {passed, score, feedback},不达标触发重规划;
  • Reporter:汇总各子任务产出与产物路径,输出最终交付物。

Planner 产出的 subtasks 长这样(来自真实运行时结构):

{
  "subtasks": [
    {"index": 0, "title": "抓取数据源", "instruction": "fetch 官方公开端点并落盘", "artifacts": []},
    {"index": 1, "title": "结构化解析", "instruction": "把原始数据整理成表格", "artifacts": ["data/parsed.csv"]},
    {"index": 2, "title": "生成报告", "instruction": "基于表格写交付文档", "artifacts": ["report.md"]}
  ]
}

Evaluator 对每个子任务验收后给出裁决:

{  "passed": true,  "score": 92,  "feedback": "数据完整,口径与目标一致"}

passed 为 false 且还没到 max_replan_rounds 上限时,系统把 feedback 喂回 Planner 重拆——这意味着任务模式会自己纠错,而不是一次性把错误答案交给你。并行度由 parallel 参数控制(默认 4,范围 1+),多个 Specialist 各自跑在独立沙箱里,互不偷看对方的文件。

关键设计是委托式自主:任务模式不做逐步人工审批(工具全部沙箱隔离),人工门保留在交互式聊天里(写文件类工具需确认)。而 Fast Path 就嵌在 Specialist 的入口处——每个子任务先问一句"这题能不能不算",能算的子任务直接零模型完成,照常出现在任务树里,只是标着 zero-model。编排层对此完全无感,这正是它"对循环透明"的含义。

三层架构:内置匹配器 → Codegen → Promote

下面这个例子故意写得简单——只是进制换算而已。重点不是它,是"任何确定性逻辑都能照此挂上去"的机制。

第一层:内置匹配器——命中即零模型

以内置的进制转换匹配器为例(fastpath.py)。一个正则识别口语化的提问,再走纯 Python 计算:

_BASE_RE = re.compile(r"(\d+)\s*的\s*(二进制|八进制|十六进制)"
                      r"|(二进制|八进制|十六进制)\s*(?:|表示|形式)?\s*(\d+)", re.I)
_BASE_MAP = {"二进制": 2, "八进制": 8, "十六进制": 16}

def _try_base_convert(text: str) -> FastAnswer | None:
    m = _BASE_RE.search(text)
    if not m:
        return None
    num_str, base_name = (m.group(1), m.group(2)) if m.group(1) else (m.group(4), m.group(3))
    base_name = base_name.replace("数", "")
    base = _BASE_MAP.get(base_name)
    if base is None:
        return None
    value = int(num_str)
    if base == 2:
        result = bin(value)[2:]
    elif base == 8:
        result = oct(value)[2:]
    else:
        result = hex(value)[2:]
    return FastAnswer(text, "base_convert", f"{num_str}{base_name}{result}。", result)

真实效果:「255 的十六进制是多少」这类问题,从正则命中到拼出答案不足一毫秒——不联网、零 token,答案格式永远一致。同类的还有安全算术求值(AST 白名单逐节点校验后再 eval)、日期推算、单位换算等十几个内置匹配器。它们的存在不是为了"秀"这几个功能,而是给你一个范本:你领域里那些"总是同一个答案"的活儿,都该长这样。

第二层:Codegen——让模型给自己写加速器

内置匹配器覆盖不了的长尾怎么办?下沉到 Codegen 层:让模型写一个满足固定契约的纯函数——

def detect(text: str) -> str | None:
    """命中返回答案字符串;未命中返回 None。"""

生成的函数经 AST 白名单沙箱验证后写入 data/fastpath_plugins/,本轮立即复用;下次同类问题再来直接命中,连模型都不调用。这是 Fast Path 最有意思的地方:第一次走完整流程(模型调用 + 代码生成 + 验证),之后加速器是模型亲手给自己造的

这里要诚实说清边界:codegen.py 的验证只做安全(AST 白名单:禁 import 除 re/math 外的模块、禁 dunder 访问、在净化过的 builtins 命名空间里执行、无 I/O 无 eval),不做正确性验证。一个语义错误的检测器照样能过沙箱、被持久化、被反复零模型调用——而且因为跑得飞快,它看起来"无比正确"。所以这一层真正的保险在第三道闸。

第三层:Promote——临时产物晋升为一等公民

data/fastpath_plugins/ 里的检测器是"候选区"。在 Plugins 页面可以审阅每个检测器的完整源码,表现稳定的可一键晋升:合并进 src/resolve_harness/generated_detectors.py 随源码分发,运行时候选副本随之删除。晋升合并保留既有记录(回归测试 test_promote_preserves_existing_detectors 锁定),不会互相覆盖。这一步由人工确认完成——它既是质量闸,也是插件从"临时产物"变成"项目资产"的关键一跃。安全靠沙箱,正确靠人眼,这是 Fast Path 不变的双保险。

端到端走查:三个查询,三种命运

光看机制容易抽象,走三个真实会发生的查询,看它们分别落在哪一层、任务树和用量长什么样。

查询 A:「255 的十六进制是多少」 → 命中内置 base_convert 匹配器。任务树出现一个 zero-model 节点,本次 token 用量精确为 0,毫秒级返回 ff。全程没唤醒模型。

查询 B:「把这批订单按金额区间分组统计」(假设没有现成内置匹配器) → 内置层返回 None,下沉 Codegen。模型生成 detect(text) 检测器 → AST 白名单验证通过 → 写入 data/fastpath_plugins/ → 立即执行返回结果。下一次同类查询直接命中插件,零模型。这就是"第一次慢、之后快"的典型路径。

查询 C:「帮我分析上个季度营收为什么下滑,写一份给老板的复盘」 → 这是开放目标,Fast Path 整层让路。进入 PSE 编排:Planner 拆成"拉数据 / 归因分析 / 写文档"三个子任务,Specialist 并行执行(各自独立沙箱),Evaluator 验收,Reporter 汇总成 report.md。任务树里这三个节点都带模型用量,没有 zero-model 标注——确定性部分(如果某步涉及计算)仍可能被 Fast Path 截走,开放部分则老老实实走模型。

三种命运对应三种成本结构:A 免费且瞬时,B 一次性成本后永久免费,C 每次都付智能的价钱。系统做的,就是尽可能把查询往左推。

从"示例"到"你的想象力"

到现在你应该看出来了:进制换算、算术、日期,都只是 seed。这套 harness 真正想卖给你的,是一个你可以自己长满确定性逻辑和领域工作流的骨架。举几个能立刻上手的脑洞——它们都是 harness 能力的直接组合,不是虚构的"别人家案例":

  • 把重复的信息整理固化成检测器:比如你每周都要把某份公开披露的结构化数据拉下来、算几个指标。第一次让 Codegen 写检测器,之后零模型秒出;稳定了晋升进源码,团队共享。
  • 用 PSE 编排多步工作流:一句"帮我调研 X 并产出对比报告",Planner 拆、Specialist 并行 fetch+分析、Evaluator 卡质量、Reporter 出文档。你只管给目标,拆分和执行交给编排。
  • 沙箱工具链拼本地小工具fetch + 写文件 + run_script 组合,能做出完全本地、数据不出机的处理流水线——适合那些"不想把数据喂给第三方"的场景。
  • 人工晋升沉淀团队资产:常用逻辑从"某次会话的临时插件"变成"仓库里的人人可用函数",知识不再散落在聊天记录里。
  • 混合编排:让 Fast Path 吃掉流程里所有确定性环节,只把真正需要判断力的部分留给模型——你的 Agent 既快又省,还更稳。

想象力落在哪,取决于你的领域。金融、文档、数据清洗、研报、内部工具……只要存在"同一输入总该有同一输出"的环节,就值得挂一道 Fast Path;只要存在"多步、可并行、要验收"的目标,就值得交给 PSE。示例只是敲门砖,门后面是你自己的玩法。

安全边界:生成的代码不能裸奔

让模型写代码并在进程内执行,安全问题必须正面回答。resolve-harness 用 AST 白名单给出双保险:

_FORBIDDEN_ATTRS = {"eval", "exec", "format", "format_map",
                    "globals", "locals", "mro", "subclasses", "init"}

_ALLOWED_NODES = (
    ast.Module, ast.FunctionDef, ast.arguments, ast.arg,
    ast.Return, ast.Expr, ast.Assign, ast.AnnAssign,
    ast.Call, ast.Name,
)

两条规则:节点白名单——语法树上只允许出现列出的节点类型;属性黑名单——禁掉已知逃逸口。

这里有一场真实的攻防:早期版本拦截了 eval/exec/__class__ 遍历,但 f-string 的 format_map 是它的孪生兄弟逃逸口——藏在普通字符串字面量里的 dunder 遍历链可以绕过属性检查。补上 format_map 拦截后,回归测试 test_format_map_dunder_bypass_rejected 把这条路径彻底锁死。

配套的 run_script 工具同样纵深防御:脚本白名单 + 强制沙箱内落盘 + 子进程环境净化(API key 等敏感变量不透传)。

安全边界的意义,不只是"防坏人"——它让你敢放开手脚做实验。正因为生成的代码过不了沙箱就写不进磁盘(save_plugin 在持久化前强制 validate_ast,拒绝任何不安全代码),你才可以在开发期大胆让模型造检测器,不必担心它偷偷干坏事。

可观测性与质量闸

快必须快得明白:

  • 任务树标注:Fast Path 命中的子任务带 zero-model 标签,一眼区分;
  • 用量归零可验证:零模型路径的 usage 是精确的 0 token,有回归测试锁定该行为;
  • 事件流:Planner 的 plan、Evaluator 的 evaluation、各子任务的执行事件全程上报,你能实时看着自己的编排"长"出来;
  • 源码可审阅:Plugins 页能查看、删除任何一个检测器。

质量上采用人机协同:AST 白名单把安全性关死,正确性由人工审阅与晋升确认把关。路线图的下一步是把人工这一步也部分自动化——持久化前对检测器跑一组已知答案的边界自测,形成"机器兜底 + 人工复核"的双保险。

复盘:一条可复用的设计原则

整条流水线沉淀下来的原则只有一句:自动化的程度,应当与验证强度成正比

  • 完全自动执行的内置匹配器:逻辑由人编写、测试覆盖,可信;
  • 模型生成的 Codegen:通过安全沙箱才准执行;
  • 要长期沉淀的 Promote:必须经过人的眼睛。

任何想用 AI 加速工程流程的团队,都可以套用这个阶梯自查:哪一步是纯代码?哪一步引入了生成物?生成物的验证强度,配得上它的自动化程度吗?

放到整个 harness 里看也是同一件事:Planner/Specialist 的智能负责处理不确定的部分,Evaluator 与质量闸负责守住确定性的部分——两层各司其职,系统才既灵活又可靠。而对你来说,这条原则也是使用手册:你挂上来的每一个检测器、每一段编排,都该按"它有多可信"来决定"它能自动到什么程度"。 想象力可以无限,验证不能偷懒。

源码导航

完整代码:https://github.com/erishen/resolve-harness

  • src/resolve_harness/graph/loop.py
  • src/resolve_harness/harness.py
  • src/resolve_harness/tasks.py
  • src/resolve_harness/fastpath.py
  • src/resolve_harness/codegen.py
  • src/resolve_harness/generated_detectors.py
  • src/resolve_harness/memory/long_term.py
  • tests/test_tasks.py · tests/test_fastpath.py · tests/test_codegen.py

什么样的任务适合走 Fast Path?

具有唯一正确答案、可确定性求解的任务:四则运算、进制转换、日期推算、单位换算等。判断标准是”同一输入永远对应同一输出”;开放式创作与需要上下文理解的问题不适合这条路径。

Codegen 生成的检测器如何保证安全?

所有生成代码必须通过 AST 白名单沙箱:语法树上只放行列出的节点类型,eval/exec/format/format_map 等危险属性一律禁止;持久化前 save_plugin 还会强制再校验一次,拒绝任何不安全代码。曾有一次针对 format_map 的字符串字面量 dunder 遍历旁路尝试,已被拦截并以回归测试锁死。

为什么 Promote 需要人工确认?

运行时插件的出身是一次性的模型输出,正确性没有形式化保证(沙箱只验安全不验对错);晋升会把它合并进源码库随项目分发,影响面从单机扩大到整个仓库,因此转正决定权留给人——先在 Plugins 页审阅完整源码,再确认晋升。

怎么知道某次回答走了零模型路径?

两个信号:任务树中该步骤带 zero-model 标注;本次调用的 token 用量为精确的 0(prompt/completion/total 均为 0),并有专门的回归测试锁定该行为。

如果一个检测器的答案不对怎么办?

在 Plugins 页可直接查看其完整源码并删除,同类问题将重新走完整模型路由;未晋升的检测器删除即彻底清除,不影响源码库。持久化前的自动边界自测已在路线图中,落地后错误检测器会在写入前被拦截。

Planner / Specialist / Evaluator 这些角色是怎么协作的?

Planner 先把目标拆解为有序子任务(结构化 subtasks JSON);每个 Specialist 进入独立的工具循环执行(可并行 fan-out,各自独立沙箱),并在入口处先尝试零模型的 Fast Path;Evaluator 对照原始目标验收,返回 {passed, score, feedback},不达标按配置轮数触发重规划;最后 Reporter 汇总产出与产物路径生成交付物。全程事件化上报,任务树实时可见。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

首页 简历 商店 Web Chat Nsbp 关于 隐私政策

@ 2026 ESN
沪ICP备2024079226号-1   沪公网安备31010502007082号