Back to Blog
Agent Harness 架构解析:从定义到解耦演进

Agent Harness 架构解析:从定义到解耦演进

Agent Harness 架构解析:从定义到解耦演进

·Unknown
本文基于 Anthropic - Scaling Managed AgentsLangChain - The Anatomy of an Agent Harness 两篇文章,结合个人对 Claude Code 源码的理解,聊聊 Agent Harness 到底是什么、怎么设计、以及它的演进方向。

一、什么是 Agent Harness

1.1 一句话定义

LangChain 给出的公式很简洁:

Agent = Model + Harness

一句话总结:如果你不是模型,那你就是 Harness。

Harness 就是模型之外的一切——调用模型的循环、工具路由、上下文管理、执行环境、安全隔离、记忆机制……所有让一个"裸模型"变成"能干活的 Agent"的基础设施代码,统称为 Harness。

1.2 Harness vs 传统 Agent 框架

不少早期 Agent 框架(典型如早期 LangChain 的 Chain/Agent 抽象)更倾向于:把模型能力封装成 预定义的工具链,由开发者编排调用顺序。

Harness 的侧重点通常不同:它更少预设模型要做什么,而是提供 通用能力原语(文件系统、代码执行、沙箱、网络访问),让模型自主决定如何组合这些原语来解决问题。

打个比方:

  • 传统框架 ≈ 给模型一份 SOP 手册,按步骤执行
  • Harness ≈ 给模型一间装备齐全的工作间,自己决定用什么工具

1.3 一个具体的例子

Claude Code 本身就是一个典型的 Harness:

┌───────────────────────────────────────────────┐
│            Claude Code (Harness)              │
│                                               │
│   ┌──────────┐  ┌──────────┐  ┌──────────┐    │
│   │ System   │  │ Tool     │  │ Context  │    │
│   │ Prompt   │  │ Router   │  │ Mgmt     │    │
│   └──────────┘  └──────────┘  └──────────┘    │
│   ┌──────────┐  ┌──────────┐  ┌──────────┐    │
│   │CLAUDE.md │  │ Bash     │  │ Memory   │    │
│   │ Loader   │  │ Read/Edit│  │ Files    │    │
│   └──────────┘  └──────────┘  └──────────┘    │
│                                               │
│             ┌──────────────┐                  │
│             │ Claude Model │                  │
│             │ (the Brain)  │                  │
│             └──────────────┘                  │
└───────────────────────────────────────────────┘

模型负责"想"(推理、规划、决策),Harness 负责"做"(执行命令、读写文件、管理上下文、隔离权限)。


二、Harness 的核心组件

LangChain 文章总结了 Harness 的六大核心组件,我结合 Claude Code 逐一说明。

2.1 文件系统(Filesystem)

文件系统是 Harness 最关键的基础原语,作用远超"读写文件":

用途说明Claude Code 实例
持久化存储跨对话运行保留信息CLAUDE.md
上下文外扩存放超出 token 限制的信息工具大段输出落盘,context 中只留摘要与文件路径
多 Agent 协作面共享工作区团队共享的 task list 文件
版本控制追踪变更、支持回退git worktree 隔离
补充说明:表格第二行里我称之为 persisted-output 的工具输出落盘策略,以及第三行里"由 TeamCreate 创建团队并绑定共享 task list 文件"的机制,均来自我对 Claude Code 源码的个人观察与推断,并非 Anthropic / LangChain 官方文档中的表述,也不是业内通用术语。

文件系统本质上是 Agent 的 外部记忆体——context window 是"工作记忆"(短期),文件系统是"长期记忆"。

2.2 代码执行(Code Execution)

与其给模型预封装 100 个工具,不如给它一个通用的代码执行能力:

预封装工具方式:
  Tool: search_files(pattern="*.go", keyword="func main")
代码执行方式:
  Bash: find . -name "*.go" | xargs grep "func main"

后者更灵活——模型可以自己组合任意 shell 命令来解决问题,不受预定义工具集的限制。Claude Code 提供 Bash 工具正是这个设计理念:给模型"手"而非"工具箱"

2.3 沙箱(Sandbox)

沙箱提供三层价值:

  1. 安全性:模型生成的代码在隔离环境中执行,不会影响宿主系统
  2. 可扩展性:沙箱可以独立伸缩,不受 Harness 进程约束
  3. 预装环境:语言运行时、CLI 工具、浏览器等,让 Agent 能自主验证工作结果

2.4 记忆与搜索(Memory & Search)

Agent 需要跨对话运行积累经验,也需要获取训练数据之外的实时信息:

run 1: 用户纠正了 coding style 偏好
    ↓ 写入 memory
run 2: Agent 直接按正确风格编码

两条路径:

  • 持久化记忆文件:把偏好、规范、经验写入文件,跨运行复用
  • 外部搜索能力:通过 web search、MCP 工具等访问训练截止之后的信息

Claude Code 的记忆层次:

  • ~/.claude/CLAUDE.md — 用户全局偏好
  • 项目级 CLAUDE.md — 项目规范
  • 单次运行的工作上下文 — 本轮对话与工具结果,默认不进入长期 memory 文件,下次启动也不会自动读到(Claude Code 会把 transcript 落盘到 ~/.claude/projects/... 供恢复与排查,但不是模型下轮默认能看到的"记忆")

2.5 上下文管理(Context Management)

长对话中 context window 会"腐烂"(context rot):越来越多无关信息稀释了有效信息。Harness 需要主动管理上下文:

  • 压缩(Compaction):压缩历史对话,保留关键信息
  • 工具输出卸载(Offloading):大段输出存到文件而非留在 context 中
  • 渐进式技能披露(Progressive Skill Disclosure):按需注入指令,而非一次性全部加载

2.6 长程执行(Long-Horizon Execution)

真实场景里,一个任务可能跨越多次对话运行、持续几小时甚至几天。Harness 需要支撑这种"长程作业":

  • 文件系统作为进度载体:把中间状态落盘,运行中断也不会从头再来
  • 规划与自我验证:让 Agent 拆解子任务、检查阶段性产出
  • 可恢复的执行循环:例如 Ralph Loop 这类模式——中断后可以基于任务清单与已产出物继续推进,而不是重跑

长程执行本质上是把前面五项组件(文件系统、代码执行、沙箱、记忆搜索、上下文管理)编排起来,让 Agent 具备"跨时段推进一件事"的能力。


三、Harness 的架构演进

这部分是 Anthropic 文章的核心洞见——Harness 设计如何随模型能力进步而演进。

3.1 早期:单容器 “Pet” 模式

最初的 Managed Agents 架构把所有组件塞进一个容器:

┌──────────────────── Container ─────────────────────┐
│                                                    │
│   Session State + Harness + Sandbox                │
│   (all coupled together)                           │
│                                                    │
└────────────────────────────────────────────────────┘

这就像养了一只"宠物"——每个容器都是独一无二的,包含不可替代的状态。问题:

  • 容器挂了 = 会话丢失:所有状态随容器一起消失
  • 调试困难:进入容器调试可能暴露用户数据
  • 启动慢:用户发第一条消息要等容器就绪,p95 延迟极高
  • 弹性差:无法独立扩展计算和存储

3.2 三层解耦:Session / Harness / Sandbox

术语澄清:下文出现的 Session(首字母大写)特指 Anthropic 架构里的事件日志组件,是一个 append-only 的持久化存储,与前面章节里表示"一次对话运行"的 session 不是同一个东西。为避免混淆,本文后半部分涉及"对话运行"的地方都改用"对话运行 / 运行 / run",组件层面统一用大写 Session 或"事件日志"。

Anthropic 的解法是将单体架构拆成三个独立接口:

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│    ┌───────────┐       ┌───────────┐       ┌───────────┐    │
│    │  Session  │       │  Harness  │       │  Sandbox  │    │
│    │           │       │           │       │           │    │
│    │ Immutable │◄─────►│  Call LLM │──────►│  Run code │    │
│    │ event log │       │ Tool route│       │ File I/O  │    │
│    │           │       │           │       │           │    │
│    └───────────┘       └───────────┘       └───────────┘    │
│                                                             │
│    Persistent store    Stateless svc     Stateless sandbox  │
│    (append-only)       (restartable)     (replaceable)      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

(Session = 不可变的事件日志;Harness = 调用模型 / 路由工具的无状态服务;Sandbox = 执行代码 / 文件操作的无状态容器。)

Session ↔ Harness 的交互语义:Harness 向 Session append 事件日志;重启后新实例通过 replay 已有事件流恢复状态。

这个设计借鉴了操作系统的思路——就像 OS 将硬件虚拟化为"进程"和"文件"这些比硬件寿命更长的抽象一样,Managed Agents 将 Agent 运行时虚拟化为比任何具体实现都长寿的接口。

3.3 Brain vs Hands 分离

解耦后最核心的变化是 Brain(Claude + Harness)与 Hands(Sandbox 以及各类执行工具)彻底分离

Harness 与对话运行的状态解耦,不再和某一个具体容器绑定(harness 自身仍然可以跑在容器里,只是它变成无状态、可重启的服务)。容器这一端变成了纯粹的"手"——无状态、可替换。如果一个容器挂了,Harness 只需将其视为一次工具调用失败,重新分配一个新容器即可。

Session 独立于 Harness 存在。Harness 崩溃不会丢失任何状态,新的 Harness 实例拿到 sessionId 就能从事件日志里接着干:

// 示意伪代码(非官方 SDK 接口,仅表达恢复语义)
const session = await getSession(sessionId);
const events = await session.getEvents({ after: lastProcessedEvent });
// 从中断点继续工作

3.4 性能收益

解耦带来了显著的性能改善——关键指标是 TTFT(Time-to-First-Token),即用户从发送消息到看到第一个响应的时间:

指标改善幅度
p50 TTFT~60% 降低
p95 TTFT>90% 降低

核心原因(基于架构推断):解耦前,推理被绑在容器生命周期上,必须等容器就绪;解耦后,Harness 作为无状态服务可以立即开始推理,不再阻塞于执行环境就绪。

3.5 安全边界

解耦还天然形成了安全隔离:

┌──────── Harness ────────┐       ┌──────── Sandbox ────────┐
│                         │       │                         │
│  User credentials  [Y]  │       │  User credentials  [N]  │
│  OAuth tokens      [Y]  │─────► │  Only op results        │
│  API keys          [Y]  │       │                         │
│                         │       │  Model-generated code   │
│                         │       │  runs here              │
└─────────────────────────┘       └─────────────────────────┘

(左侧 Harness 持有用户 credentials / OAuth tokens / API keys;沙箱里只能看到操作结果,credentials 本身不进入沙箱,模型生成的代码也只在沙箱内执行。)

Anthropic 文中提到两种方向确保 credentials 不进入沙箱(以下具体落地细节是我基于架构契约做的推测,原文并未展开到这一层):

  1. 初始化时绑定(bundling):credentials 在容器初始化时绑定为资源(例如 Git repo token 绑为 repo 资源的一部分),沙箱只能通过上层抽象(如 git 命令)隐式使用
  2. 代理访问(external vault):工具调用通过专用代理获取 credentials,代理从外部 vault 读取后只把操作结果回传,credentials 本体不进入 sandbox、也不会暴露给模型生成的代码

3.6 多 Brain 多 Hands

解耦后的架构给"多对多"扩展留下了空间(以下结构是基于接口契约的推演,并非 Anthropic 原文给出的明确结论):

  Brain A ──┐                   ┌── Hand 1 (container)
            ├── Session ──┐     │
  Brain B ──┘             └─────┼── Hand 2 (container)
                                │
                                └── Hand 3 (MCP Server)
  • 多 Harness 可以作为无状态服务连接到同一个 Session
  • 多 Sandbox 被视为可互换的工具
  • 理论上 Brain 之间还可以相互"传递" Hands,从而支持更复杂的多 Agent 协作

四、设计哲学与启示

4.1 Harness 的"过期"问题

顺着 Anthropic "稳定接口 + 可替换实现"的思路再往前推一步,会发现一个更有意思的问题:Harness 难免会编码对模型能力的假设,而这些假设会随模型进步而过时(这一层是我在原文解耦思想上的延伸,并非 Anthropic 文章直接给出的结论)。

举一个容易理解的场景(以下为说明性假想例子,用来演示"假设会过期"这一模式,并非原文出现的具体案例):假设早期模型存在"上下文焦虑"——对话变长、接近上下文上限时倾向于提前"收尾"。为此 Harness 加入上下文重置 + 结构化交接(handoff artifact)机制来规避这类行为。当换上上下文处理能力更强的新模型时,这套 workaround 可能就会从必要兜底退化成无意义的复杂度,甚至反过来拖累模型。

教训:面向模型限制写的 Harness 代码,是技术债务的温床

4.2 面向稳定接口而非面向实现

Anthropic 原文给出的应对策略是 Future-Proofing Through Abstraction——构建一组比任何具体 Harness 实现都长寿的稳定接口。顺着这个思路往前推一步,可以把它看作一种"元接口层"的设计(下图中的命名是个人对这一思路的概括,并非原文术语):

  • 对接口有强主张:Session 必须是 append-only 的事件流、Sandbox 必须是无状态可替换的
  • 对实现保持中立:具体的 Harness 逻辑(Claude Code、task-specific agent、未来新形态)可以随意替换

    稳定接口层(Session / Sandbox 契约不变) │ ├── Claude Code Harness(今天的实现) ├── Task-Specific Harness(针对特定场景优化) └── Future Harness(随模型进步而演进)

4.3 对我们的启发

如果你在构建 Agent 应用,以下几点值得思考:

1. 不要在 Harness 里 encode 太多假设

// Bad: 假设模型不擅长长上下文
if (context.length > 4000) {
  context = summarize(context);  // 强制压缩
}
// Better: 让模型自己判断是否需要压缩
tools.push({
  name: "compact_context",
  description: "当你觉得上下文太长时调用"
});

2. 从第一天就考虑对话运行的持久化

哪怕是单机应用,也应该把对话历史存到文件/数据库,而非仅保留在内存中。好处:

  • 崩溃恢复
  • 跨设备续接
  • 事后分析与调试

3. 安全边界设计前置

Credentials 管理要从架构层面解决,而非靠"小心编码"。核心原则:模型生成的代码能触及的环境里,不应该存在任何敏感信息

4. 为模型能力增长留空间

今天你可能需要复杂的 ReAct 循环、Chain-of-Thought 强制、多步验证。但随着模型能力提升,这些可能都变成多余的。设计时问自己:如果模型突然变强 10 倍,我的 Harness 哪些部分会变成废代码?


五、回顾与总结

维度LangChain 视角Anthropic 视角
关注点Harness 是什么(组件与职责)Harness 怎么演进(解耦与架构)
核心观点Agent = Model + Harness将 Brain 从 Hands 中解耦
设计思路提供通用原语而非预定义工具构建比实现更长寿的接口
对模型的态度赋能模型自主解决问题不要编码对模型的假设
关键创新Filesystem 作为核心原语Session/Harness/Sandbox 三层分离

两篇文章共同指向一个趋势:Harness 在变"薄"。随着模型越来越聪明,Harness 的职责从"引导模型做对事"逐渐转向"给模型提供做事的能力"。最好的 Harness,是模型感觉不到它存在的那种。


参考资料