Deep Read

AI Harness 工程

什么是 Harness

Harness 原意是"马具、挽具"——马有力气,但要拉车,得靠缰绳和轭具把力气导向正确的方向。AI 领域借这个词,指包在模型外面、让模型能实际完成任务的那套工程系统。

大语言模型本身只是一个无状态的函数:输入一段 token 序列,输出下一段 token 序列。没有记忆,不能执行代码,不能读文件,一次调用结束后什么都不剩。直接拿裸模型去"写代码",它只能凭想象输出一段文本,既没法验证对错,也改不了磁盘上的文件。

Harness 补的就是这些。以 Claude Code 为例,模型只负责决定下一步做什么,剩下的全是 Harness 在干:把用户请求、系统提示、项目上下文拼成模型输入;解析输出里的工具调用并真正执行(读文件、跑命令、改代码);把执行结果喂回模型形成循环;管理权限、上下文窗口和会话状态。模型是引擎,Harness 是整辆车。

为什么需要 Harness 工程

最直接的原因:模型的输出只是文本。"帮我修这个 bug"需要有人真正去读代码、改文件、跑测试,Harness 负责把模型的意图翻译成动作,再把动作的结果翻译回模型能读的文本。没有这一层,模型再聪明也只是纸上谈兵。

但补齐交互能力只是起点,Harness 值得单独当一门工程来做,有三个更深的理由。

同一个模型,Harness 不同,表现天差地别。 SWE-bench 这类基准上,同一个模型在不同 Harness 下的得分能差出十几个百分点:给模型什么工具(只有 bash,还是有专门的搜索、编辑工具)、工具返回怎么组织(原始 dump 还是结构化摘要)、出错时怎么引导恢复、上下文快满时保留什么丢掉什么,每一项都影响成绩。模型能力是上限,Harness 决定能兑现多少——厂商发新模型时公布的 agentic 跑分,都是在自家精调过的 Harness 上跑出来的,换个糙的直接掉一截。

安全边界必须建在模型外面。 模型可能被 prompt injection 诱导执行危险操作,也可能单纯犯错,比如把 rm -rf 的路径写错。所以模型的输出永远只能当"建议",是否执行、如何执行由 Harness 裁决:权限系统决定哪些操作直接放行、哪些要人确认,沙箱把命令关在隔离环境里跑,所有动作留痕、可回放可追责。

长任务需要工程化的状态管理。 上下文窗口有限,而真实任务(重构一个模块、查一个线上问题)可能持续几小时,中间信息远超窗口容量。Harness 要做上下文压缩(快满时把历史摘要化,留结论、丢过程)、外部记忆(长期有效的信息写进文件,下次会话读回来)、子任务隔离(搜索类的脏活派给子 Agent,主流程只拿结论,不被中间过程污染)。

Harness 的核心组成

一个完整的 Agent Harness,拆开看就是一个循环加几个子系统:

image.png

Agent Loop

核心就是一个循环:

while True:
    response = model(messages, tools)
    if response.没有工具调用:
        return response.text          # 任务完成
    for call in response.tool_calls:
        result = execute(call)         # Harness 真正干活
        messages.append(result)        # 结果喂回去

模型每轮只决定下一步,循环持续到模型认为任务完成。所有 Coding Agent——Claude Code、Codex CLI、Cursor Agent——骨架都是这个循环,差别全在细节。

工具层

工具是模型的手脚,工具接口设计是 Harness 工程里最见功力的地方。粒度要对:太细(open_file/seek/read_line)干一件事要调很多次,太粗(do_everything)模型控制不了行为,"读文件带行号"、"精确字符串替换编辑"、"正则搜索"这种粒度是实践里收敛出来的。返回信息要为模型设计:报错要说清哪里错了、该怎么改,而不是抛一个裸异常——工具返回是模型的眼睛,返回质量直接决定下一步决策的质量。另外,工具的 description 就是模型的使用手册,写得含糊模型就会用错。

上下文工程

每次调用模型前,Harness 要决定把什么放进上下文:系统提示、项目配置(CLAUDE.md 之类)、对话历史、工具结果、相关文件。原则是每个 token 都要挣到它的位置——无关信息不只浪费,还会稀释模型的注意力、诱发幻觉。

权限与沙箱

信任边界在 Harness,实践上分层处理:只读操作(读文件、搜索)默认放行;写操作、执行命令要用户确认或匹配白名单;删除、推送、发布这类高危操作强制人工确认。

Hooks

好的 Harness 会在关键节点留钩子——工具调用前后、会话开始结束、模型停止时——用户挂脚本进去做格式化、lint、通知。这让 Harness 从一个封闭产品变成可编程的平台。

怎么用

大多数场景不用自己造。要干活,直接用成熟的 Coding Agent(Claude Code、Codex CLI、Cursor);要在自己的产品里内嵌 Agent,用 Claude Agent SDK、OpenAI Agents SDK 这类框架,Loop、工具执行、上下文管理都封装好了,只需定义工具和提示;要把内部系统(数据库、工单、监控)接进现有 Harness,写一个 MCP Server 暴露出去,任何支持 MCP 的 Harness 都能用。

理解原理最好的方式还是亲手写一个。一个能跑的最小 Harness 不到一百行:

import anthropic, subprocess

client = anthropic.Anthropic()
tools = [{
    "name": "bash",
    "description": "在 shell 中执行命令,返回 stdout/stderr",
    "input_schema": {
        "type": "object",
        "properties": {"command": {"type": "string"}},
        "required": ["command"],
    },
}]

messages = [{"role": "user", "content": "当前目录下哪个文件最大?"}]

while True:
    resp = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=4096,
        tools=tools,
        messages=messages,
    )
    messages.append({"role": "assistant", "content": resp.content})
    if resp.stop_reason != "tool_use":
        print(resp.content[0].text)
        break
    results = []
    for block in resp.content:
        if block.type == "tool_use":
            # 真实系统在这里做权限检查,而不是直接执行
            out = subprocess.run(
                block.input["command"],
                shell=True, capture_output=True, text=True, timeout=60,
            )
            results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": out.stdout + out.stderr,
            })
    messages.append({"role": "user", "content": results})

这就是全部骨架。从这个雏形到生产级,中间隔着的正是"Harness 工程"这几个字:权限、沙箱、上下文压缩、错误恢复、并行工具调用、子 Agent 调度、可观测性。

真要做,几条实践经验:先跑通循环再优化单点,瓶颈往往不在模型,而在工具返回质量、上下文组织这些外围;改动任何部分(提示词、工具描述、压缩策略)都要在固定任务集上回归,否则改进全凭感觉;失败案例比成功案例值钱,看 Agent 卡在哪一步、为什么绕弯路,通常能定位到某个工具返回不够或某段提示有歧义;能用普通代码做的(格式化、lint、模板填充)就别让模型做,模型只处理需要判断的部分——更快、更便宜、更可靠。

模型的进步是厂商的事,Harness 的打磨是使用者的事。在模型能力趋同的阶段,Harness 工程正是拉开差距的地方。