ESSAY·

Mini Harness:模块装配与可恢复执行

用本地工单任务装配模型适配器、MCP 工具、SQLite 状态和独立验收器。各模块按同一个任务契约接线;固定响应实验验证控制与恢复,不代表已经验证真实模型的任务能力。

最近修订:2026.09.07

阅读导航 · 本篇目录

用本地工单任务装配模型适配器、MCP 工具、SQLite 状态和独立验收器。各模块按同一个任务契约接线;固定响应实验验证控制与恢复,不代表已经验证真实模型的任务能力。

要搭一个能完成任务的 Harness,第一步是画清模块,以及它们交换什么。实战采用一个具体起点:单用户、单 Worker、Python、本地执行、允许重启恢复。在这个范围内,优先选择少量显式函数、严格数据校验和 SQLite;出现明确瓶颈后再换组件。

八个模块怎样拆

模块输入 → 输出必须负责的判断实验中的落点
任务契约用户目标 → 可验收目标、允许动作什么才算完成lab.py: execute() 的 expected 和运行配置
上下文组装契约、任务材料、已知状态 → 模型输入哪些信息本轮需要,来源是否可信model.py: propose(),目前只输入固定任务
模型适配模型输入 → 候选动作、调用元信息响应是否完整,工具名与字段是否有效propose()、parse_response()、validate_action()
运行控制候选动作、状态 → 下一步或终态继续、等待、失败、恢复、预算耗尽lab.py: execute()
工具执行工具名、参数 → 结构化结果调用什么,如何报告失败session_for()、call() 和 server.py
执行策略动作、操作者配置 → 允许或拒绝动作是否在授权范围内目标一致性检查、execute_write;尚无系统沙箱
状态存储契约、意图、事件 → 可恢复记录重启后哪些事实仍成立store.py: Runs,以及服务端 Tickets
结果验收可信目标、实际资源 → 通过或失败实际交付是否符合要求工具查询后对照 expected

这些模块的依赖方向应清楚:模型返回提案;运行器决定是否派发;工具改变资源;验收器读取资源。运行器把关键状态和事件写入存储。模型输出不能自行变成执行许可或验收标准。

Mini Harness 的运行器接收模型提案,经策略检查后通过 MCP 调用工具服务;Runs 保存意图与进度,Tickets 保存工单与操作键,查询结果交给验收器。

当前适合这个起点的默认组合

“最佳”必须有适用条件。下面是面向当前教学场景的工程建议,不是经过统一基准测试的工具排行榜。

模块新项目的默认选择为什么先这样选何时替换或扩展
任务与数据契约Python 数据结构 + Pydantic 或严格校验函数字段、长度、枚举和额外字段策略容易核对跨语言共享时统一维护 JSON Schema
上下文显式消息列表 + 当前任务材料 + 最近工具结果可以检查每条信息为什么进入模型材料超出预算且检索确实提升验收率时增加检索
模型一家供应方的官方 SDK,封装成小适配器先稳定输出协议、错误分类和用量记录在同一任务集上证明第二模型有收益后加路由
循环显式状态机或有步数限制的循环中断点和终止条件容易定位分支、人工暂停和恢复节点增多时考虑 LangGraph
工具同进程先直接调用函数;跨客户端共享时用 MCP SDK本地函数调试简单,MCP 解决跨进程契约多用户共享服务再引入 HTTP、身份与服务治理
策略与隔离固定工具白名单、参数约束、执行前检查可以限制当前业务动作一旦开放 Shell 或不可信代码,就加入操作系统隔离
存储SQLite,运行状态与事件同事务写入单 Worker 容易部署、检查和重放多 Worker 时增加所有权与并发控制,按需迁移数据库
验收与观测确定性断言 + 独立资源查询 + 有序事件能指出哪项要求失败语义任务增加人工标注样本;跨服务再加 Trace

官方 SDK 是新项目建议;配套实验为展示原始请求,在线模型适配器使用 Python 标准库 HTTP。实验的严格校验也由普通函数实现,不要求先引入抽象框架。

LangGraph 的 checkpointer 保存线程运行状态,store 保存跨线程信息;它可以承担更多状态编排,但不会自动替业务写入提供幂等保证。是否采用它,取决于运行流程复杂度。LangGraph 持久化文档

用什么任务把它们组装起来

实战的第一块可运行切片是:给定 source 与 title,创建一张本地待核验工单,重启后同一业务操作仍只对应一张工单。

这个任务的执行路径已经确定,因此它是单动作工作流:模型负责产生结构化提案,运行器执行固定的查询、创建和验收步骤。实际业务中,如果字段已经完全确定,可以直接调用创建函数。这里保留模型适配,是为了单独练习生成结果如何进入受控执行链路。

在此基础上,真正需要 Agent 决策的扩展任务可以是“读取三份材料,比较冲突,生成待核验问题,再创建工单”。这时模型要根据工具结果选择下一步,才需要多轮循环;模型与循环给出循环接口与消息往返,本 Mini 实验包尚未实现该扩展。架构对照实验另外实现了读取两份资料、根据观察推进、批准后创建的控制链路,模型仍用固定响应,便于隔离编排差异。

从模型到验收的装配顺序

顺序动手内容这一层完成的标准
1. 模块与选型确定八项职责、默认组件和任务边界每项职责能对应到代码或明确的待实现项
2. 模型与循环定义上下文、动作 Schema 和循环退出条件普通文本、截断、多动作不能误入执行
3. 工具与策略接入查询、创建及 MCP 传输工具响应可校验,写入许可由应用决定
4. 状态与验收保存意图,加入幂等创建与恢复查询三个退出点都能得到可解释的资源结果
5. 组装与运行跑命令、读数据库、替换一个模块正常路径和故障路径都有实际证据

Skills、长期记忆和多 Agent 都是可选扩展。只有重复流程值得沉淀、跨任务信息确实有用、任务拆分收益超过协调成本时,再分别接入。关于循环和任务边界,可对照Harness 工程主线继续阅读。

需要比较自写循环、图式编排与持久工作流时,继续阅读架构选型与对照实验。

从任务契约构造上下文

工单实验的可信目标只有两个字段:

{"source":"source-001","title":"核验 Agent Harness 资料"}

运行 ID、数据库位置、执行开关和派发预算由应用另外保存。它们不属于模型可以改写的参数。上下文组装器在这个阶段只需要任务说明、目标字段和可用工具定义。

扩展为资料研究任务时,按四层组织输入:稳定的任务规则;当前用户目标;带来源标识的材料;本轮状态和最近工具结果。检索材料中的指令应按资料内容处理,不能用它改写执行策略。长输出保留摘要与可重新读取的引用,原始证据另存;不要把每轮日志全部追加进去。

默认先用显式消息列表。只有在真实任务中发现“相关材料找不到”或上下文预算不足,才增加检索、压缩或分层记忆。这样可以判断收益到底来自模型、材料还是运行逻辑。

模型适配器输出什么

实验中的实际接口是:

# model.py 中已有的接口;provider 默认为 fixture
propose(expected, provider, model) -> (action, metadata)

action 必须恰好包含非空的 source、title,每项长度不超过 300 字符。metadata 保存模型来源;在线模式还记录响应 ID、模型和用量,便于排查同一动作是怎样产生的。

parse_response() 检查停止原因是 tool_use,且恰好出现一个名为 propose_review_ticket 的工具调用;随后由 validate_action() 检查字段。运行器最后把动作与可信目标比较。Schema 正确只能说明结构合规,不能证明目标正确。

模型返回处理方式理由
一份完整、字段正确的提案保存后进入运行器提案获得了明确结构
普通文字“已创建”拒绝没有可执行提案,也没有资源证据
被 token 上限截断的响应拒绝不能按部分输出猜测动作
两个工具调用拒绝超出本实验单动作契约
增加 approved 或 operation_key拒绝模型不能扩充自身权限或改变去重标识
字段合法,但标题与目标不同运行器拒绝结构正确仍可能做错任务

新项目可用供应方官方 SDK 处理连接与响应类型,但应保留这层应用契约。工具名称、用途说明和输入 Schema 是模型选工具的依据,名称对上并不足以约束参数语义。Claude 工具定义文档

现有实验是一轮提案解析。下文的多轮循环属于扩展设计,不应据此认为代码已自动研究资料。

多轮 Agent Loop 应怎样接

如果任务需要“读材料以后再决定查哪里”,单次 propose() 就不够。推荐把适配器扩展为接收消息历史和本轮可用工具,返回统一的三类结果:候选工具调用、最终答案、无法继续。循环控制不依赖供应方具体的停止原因名称。

下面是设计伪代码,展示模块连接;它不是实验包中可直接调用的 API:

读取已保存的任务、状态和消息
重复,最多 N 轮:
    先处理历史未确认写入:只读对账或等待,不生成替代写入
    检查任务截止时间、剩余额度和取消状态
    组装本轮上下文,调用模型,校验完整响应
    若模型提出最终答案:执行独立验收,再决定成功或继续
    若模型提出工具调用:
        校验工具与参数,检查当前执行权限
        为业务动作确定稳定操作键,保存意图与预算消耗
        仅在可以安全执行时派发工具
        保存工具结果,按供应方协议关联原 tool call ID
        将模型的调用消息与对应工具结果加入下一轮上下文
    若无法继续:保存原因并退出
轮数耗尽:保存 budget_exhausted,交给用户或后续流程

最容易遗漏的是工具结果回传。只调用工具再打印结果,不构成“观察结果后再决策”的循环。另一处是身份:供应方的 tool call ID 用于消息关联,业务操作键用于副作用去重,两者不能混用。一次写入在重试时应复用同一业务键;不同业务动作必须分配不同键。

预算怎样放进组合

至少区分模型请求次数、工具派发次数和任务总截止时间。模型超时、工具超时、任务超时也要分别处理。写入超时可能意味着结果未知,应进入对账流程,不能一律重试。

配套实验已经持久化工具派发预算,上限为 2;模型适配器是一次非流式请求,尚无持久化模型预算、多轮消息或总截止时间。扩展循环时先补这些控制,再增加自动重试;否则一次重启就可能重置额度。

先换模型,保持其他模块不变

默认 fixture 返回固定响应,用来稳定测试运行器;anthropic 是可选的在线提案适配器。两者输出同一份动作契约,因此工具、存储和验收可以复用。

选模型时,用同一批任务比较合法提案率、目标符合率、完成成本和延迟。格式经常出错时先检查 Schema 与工具说明;提案正确但资源错误时检查执行和验收。不要把所有失败都归因于模型能力。更多网关设计见模型网关。

先定义两个业务工具

本实验只暴露两个工具,把读取和写入分开:

工具参数输出副作用
lookup_review_ticketoperation_key{"ticket": null} 或工单对象读取
create_review_ticketoperation_key, source, title{"ticket": {"id": 整数, "action": {...}}}创建,或返回同键的既有工单

同键同参数返回同一资源;同键不同参数拒绝执行。这条规则由服务端 Tickets.create() 和唯一约束落实,不能只写在工具说明里。

工具层不要顺便判断任务完成。创建接口可能成功创建了错误标题;是否符合任务仍由验收器对照原始目标判断。工具错误也要显式返回或抛出,不能把失败信息包装成看似成功的业务对象。

把本地函数包成 MCP 服务

server.py 使用官方 Python SDK 中的 FastMCP,将 Tickets.lookup() 与 Tickets.create() 包装成两个工具。返回值通过 TicketEnvelope 提供结构化 ticket 字段。

客户端的连接顺序是:启动传输 → 建立 ClientSession → initialize() → 读取工具目录 → 检查需要的工具 → 发起调用。实验的 session_for() 管连接生命周期,call() 管工具结果解析,execute() 管业务流程。这三层分开后,换传输不必改任务验收。

# 在实验目录中使用的真实客户端接口片段
async with session_for(root) as (session, version):
    ticket = await call(
        session,
        'lookup_review_ticket',
        {'operation_key': 'review-001:ticket'},
    )

root 是状态目录的 Path;完整运行入口见组装与运行。调用返回后,call() 先检查 isError,再检查 structuredContent 中是否包含 ticket。MCP 的工具结果可以包含文本与结构化内容;本应用主动选择后者作为机器判断接口。MCP 工具规范

连接、工具调用和业务验收分别有明确责任。查到既有工单时可直接验收,无需再次创建。

stdio 与 HTTP 如何选择

条件默认组合应验证的边界
Harness 自己启动本地工具服务SDK stdio 客户端 + 子进程 FastMCP启停、退出码、标准输出不能混入普通日志
多个客户端访问共享工具服务Streamable HTTP + 服务身份与认证会话、超时、代理、访问控制与资源归属
一次性同进程业务函数直接调用函数参数、错误、副作用与结果契约

实验固定 mcp==1.29.1 与 MCP 2025-11-25 协商基线,用于复现已记录结果,不把这个固定版本称为所有新项目的最佳版本。对应 SDK

stdio 测试会真正启动服务进程;HTTP 测试使用本机回环地址。两者复用工具逻辑,但 HTTP 当前只覆盖正常路径。生产 HTTP 还需要认证、会话失效、撤权和代理环境测试。本机监听不提供身份认证。

执行策略应插在哪里

顺序是:校验模型动作 → 检查业务目标 → 检查执行许可 → 保存派发意图 → 调用工具。策略检查应紧邻实际派发;恢复后也要重新判断当前是否允许新增写入。

实验里,--execute 控制是否允许创建本地工单,查询不需要该开关。操作键由运行器根据 run ID 生成,数据库路径由命令行配置。模型只提供工单内容,没有通用 Shell,也不能指定任意文件路径。

这是一份小型业务白名单。将来增加文件或 Shell 工具时,需要同时加入路径限制、环境变量过滤、网络范围和操作系统隔离;仅在提示词里写“不要越权”不能限制进程权限。Hook 可以在配置匹配的调用路径执行检查,但不能替代服务端授权或沙箱。执行安全篇

怎样替换为真实业务系统

保留运行器面对的查询、创建和结果接口,在服务端替换 Tickets 的存储实现。接 CRM、任务系统或工单 SaaS 前,先确认四件事:是否支持业务幂等键;是否能按该键查询;写入后多久查询可见;冲突与限流怎样报告。

两份存储各管什么

存储内容它能回答的问题
runs.sqlite任务契约、状态、已保存提案、派发次数、有序事件Harness 原本打算做什么,走到了哪里
tickets.sqlite操作键、资源 ID、工单字段业务资源到底是否存在,内容是什么

实验中客户端维护 Runs,工具服务维护 Tickets。分开存储是为了真实呈现“两次提交不能靠同一个本地事务包住”的问题。运行状态和事件在运行库内同事务提交;它们与工单创建不构成跨库原子事务。

状态机只保留有行动意义的状态

状态下一次应该做什么
new生成并校验提案
planned使用已保存提案,进入资源查询
reconciling根据业务键查工单,判断是否还需创建
waiting_execution等待允许新增写入;资源已存在时可以只验收
dispatching派发意图已落盘;重启后先查询,不能假定未写入
reconcile_required派发预算用尽;继续查询或转人工处理
succeeded / failed返回已保存的终态

interrupted 是命令行捕获异常后的输出摘要,不是数据库里的新状态。异常发生时,数据库可能仍停在 dispatching。恢复应以持久化记录为依据。

本实验成功终态重读会直接返回,不持续监测工单以后是否被删除。若业务要求“始终存在”,应另设巡检任务,不能把一次验收扩大成永久保证。

一次写入怎样组装

实际执行顺序如下:

  1. Runs.open() 保存任务契约;同一个 run ID 更换目标、供应方或模型时拒绝恢复。
  2. 提案校验成功后保存;恢复时复用提案。
  3. 根据 run + ':ticket' 得到稳定操作键,查询工单。
  4. 如果工单不存在,检查执行许可和剩余派发预算。
  5. 用 Runs.record() 在同一事务内保存意图、事件并消耗一次预算。
  6. 调用创建工具,再用查询工具读取实际工单。
  7. 验收 ID 类型及字段与原始目标一致后,保存成功终态。

第 5 步记录的是派发尝试预算,因此即使进程在网络调用前退出,也已经消耗一次。这个保守选择可以避免重启无限重置额度;它不是服务端实际执行次数的精确统计。

服务端创建则在 SQLite 事务中检查操作键,并以唯一约束防止重复资源。同键出现不同参数会报冲突。事务与连接管理的具体行为应以 Python SQLite 接口为准。Python sqlite3 文档

验收器为什么必须独立

本实验验收的是“待核验工单创建正确”,判定式是:查询到工单、ID 为整数、action 与可信 expected 相等。它没有核验来源内容的真实性。

验收独立指:标准来自用户目标,证据来自资源读取,不接受模型一句“已完成”作为成功条件。这里查询与创建仍由同一测试服务提供,所以它可以发现丢响应和错误内容,不能抵抗恶意服务伪造结果。

换成代码任务时,验收可以是指定测试、构建产物与 Diff;换成研究任务时,需要把来源是否支持结论拆成可复核的检查。验收复杂度跟任务走,不能用“工具调用成功”统一代替。

用三个退出点检查恢复

故障位置退出时资源数恢复动作最终资源数 / 派发预算消耗
客户端保存意图后、调用前0查询为空,获准后再次派发1 / 2
服务端提交后、响应前1查询到资源并验收1 / 1
客户端收到结果后、验收前1查询到资源并验收1 / 1

这三种情况在实验中通过真实进程退出重现,不只是函数抛异常。恢复命令要复用状态目录和 run ID;把状态目录换掉,就不是恢复原运行。

什么时候要换持久化方案

单 Worker 可以使用这里的小状态机。多 Worker 会引入新的问题:谁拥有运行、谁能消耗预算、旧 Worker 恢复后是否还能写。此时需要租约、版本或 fencing 等所有权设计,单靠工单唯一键只能阻止同键重复资源,管不了调度竞争。

分支、暂停和消息历史增多时,可以把运行状态交给工作流框架;工具的幂等和业务验收仍需保留。先以并发与一致性中的反例确定需要的保证,再换数据库或编排器。

本实验创建一张本地待核验工单:来源为 source-001,标题为“核验 Agent Harness 资料”。运行后应只存在一条字段匹配的工单;写入后进程退出,再次运行也不能重复创建。下面按模块安装、执行并检查这个结果。

下载 Mini Harness 实验包。包内默认使用固定模型响应,MCP 子进程、传输和 SQLite 写入真实执行。

先对照装配清单

lab.py: execute()               组装入口与运行状态机
  ├─ model.py: propose()       上下文、模型调用与严格提案解析
  ├─ store.py: Runs            契约、提案、意图、预算与事件
  ├─ session_for() → call()    MCP 连接与结构化结果
  │    └─ server.py            查询、创建两个工具
  │         └─ store.py: Tickets   幂等业务资源
  └─ 查询结果对照 expected       完成验收与记录终态

没有单独的策略服务:目标一致性检查与 execute_write 在运行器内。没有独立上下文框架:单动作任务的输入在 propose() 内组装。先保留这种紧凑实现,职责增长后再拆文件。

正常路径用两条命令完成。响应丢失后的路径复用运行记录和操作键,验收回到真实工单。

安装并验证各模块契约

解压进入 harness-integration 目录;已记录的实验环境为 Python 3.11.9。以下使用 macOS/Linux 路径:

python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python -m unittest discover -p 'test_*.py' -v

Windows 使用虚拟环境中的 Scripts/python.exe。固定依赖包含 mcp==1.29.1;客户端检查协议基线 2025-11-25。安装需要网络,默认测试不调用外部模型。

16 项测试分为:8 项模型输入输出边界、2 项默认与正常运行、3 项真实退出恢复、2 项目标一致性、1 项本机 HTTP 正常路径。测试文件与 VERIFICATION.md 提供对应证据。

按两步完成一次任务

.venv/bin/python lab.py --state-dir ./state-demo
.venv/bin/python lab.py --state-dir ./state-demo --execute

第一条把模型提案交给状态库,查询为空后返回 waiting_execution。第二条复用已保存提案,允许创建,独立查询通过后返回 succeeded。事件中应依次出现提案保存、会话初始化、执行意图和验收通过;跨两次运行会有多次会话初始化。

这时检查 attempts 为 1,并读实际资源:

.venv/bin/python - <<'PYCODE'
import sqlite3
with sqlite3.connect('state-demo/tickets.sqlite') as db:
    print(db.execute('SELECT id, op, payload FROM tickets').fetchall())
PYCODE

应只有一行,操作键为 review-001:ticket,字段匹配默认目标。查询数据库用于学习和测试;在真实远端系统中应使用受授权的资源查询接口。

从代码调用组装入口

在实验目录另存 run_example.py,即可用不同任务调用同一组模块:

import asyncio
from pathlib import Path
from lab import execute

result = asyncio.run(execute(
    Path('state-example'),
    run='review-002',
    expected={'source': 'source-002', 'title': '检查报告的来源'},
    provider='fixture',
    execute_write=True,
))
print(result['state'], result['attempts'])

用 .venv/bin/python run_example.py 运行。这个接口示例对应现有实现;复用同一 run ID 时保持任务配置一致。

让模块在退出后重新接上

每个故障选择一个从未使用过的状态目录:

.venv/bin/python lab.py --state-dir ./state-crash --execute --fault server-after-commit
.venv/bin/python lab.py --state-dir ./state-crash

第一条预期非零退出;第二条查询已提交的工单并验收,attempts 仍为 1。再分别尝试 client-after-result 与 client-after-intent:前者恢复只需查询;后者资源尚不存在,恢复命令需要追加 --execute,最终 attempts 为 2。

检查状态与验收的资源数表,而不只看退出码。已经成功的运行直接返回终态,所以旧的成功目录不能用于重新触发故障。

第一次替换:接在线模型

在终端设置 ANTHROPIC_API_KEY 和账号实际可用的 ANTHROPIC_MODEL,再运行:

.venv/bin/python lab.py --state-dir ./state-live --provider anthropic --model "$ANTHROPIC_MODEL"

这会把合成任务发往 Anthropic Messages API,并可能产生模型费用。默认只保存提案和查询;确认动作后,保留相同参数追加 --execute。其他模块无需改动。

本次没有调用在线模型,不能据此给出真实模型的成功率、费用或延迟。接入后应另外保留响应类型、提案通过率和用量证据,密钥与完整敏感输入不要进入事件日志。

下一步按需求扩展哪一块

你要增加的任务能力先改的模块增加的验收证据
读材料后再决定下一步模型接口、消息历史、有界循环工具结果进入下一轮,轮数和总截止时间有效
更换真实工单系统工具服务与幂等映射响应丢失后能查询;同键同参数不重复
长任务暂停后继续状态存储、消息与预算检查点重启不丢决定性上下文,不重置额度
开放文件与 Shell执行策略、沙箱越界路径和网络动作被执行层拒绝
多 Worker 调度所有权、并发状态更新旧 Worker 不能继续提交过期动作
引入 Skills 或记忆上下文组装与来源管理对相同任务集确实提高验收率或减少成本

第一版的完成标准是:你能替换模型或工具,同时保持任务契约、执行策略、恢复和验收不被绕开。它仍是单用户、单动作的本地基线;上表提供了从这块切片扩展到多轮系统的具体路线。

← 返回文章目录沿主题继续阅读 →