收尾篇 · 构建你自己的 Agent Harness

Claude Code; Agent Harness; AsyncGenerator; 架构设计 3254 字 17 min read

这个专题从”范式转移”讲起,一路拆过配置、权限、钩子、Skills、子智能体、Plan 模式、乃至和 Codex 的哲学对比。拆解的意义不在于记住某个常量或某张流程图,而在于——当你要自己搭一套 Agent 时,知道每个决策点有哪些选项、为什么这么选。

这篇是收尾。上半部分把散落各篇的子系统重新装回一个整体,给一条能落地的六步路线图;下半部分回到最实际的问题

Windows 上,怎么从零把环境搭到开箱即用。

一、先问自己
Harness 吗

不是每个用到 LLM 的场景都需要 Agent Harness。搭一整套对话循环、权限管线、上下文压缩,是有成本的。判断的关键就一句话:

如果你的系统只需要 LLM 做”输入 → 输出”的转换(翻译、摘要、分类),用简单的 API 调用就够了。如果你的系统需要 LLM 做”观察 → 思考 → 行动 → 再观察”的循环,才需要 Agent Harness。

展开成三个维度:

维度简单 API 调用Agent Harness
交互轮次单次请求-响应多轮自主循环
工具需求无,或仅单轮 Function Calling多种工具 + 权限控制
上下文管理手动拼接 Prompt自动压缩、记忆提取
错误恢复重试多层恢复(断路器、降级、压缩)
成本敏感度低(单次调用)高(长会话累积)
安全要求低(无副作用)高(文件操作、命令执行)

满足下面任意一条,就该考虑 Harness

、涉及文件/命令/网络等副作用、对话可能跨几十上百轮、要在多个环境(CLI/IDE/SDK)保持一致行为、需要精细的 token 预算控制、需要可观测性追踪每一步决策。

二、把五大原则装回去

第一篇讲过 Claude Code 的五大设计原则。当时它们是”读懂别人架构”的钥匙,现在它们是”设计自己架构”的准绳。逐条对应到工程决策:

1. 循环优于递归。 核心对话用 while(true) 循环 + 状态对象,而不是递归调用。三个理由

(递归要回退整个调用栈)、中止信号在循环顶部检查最自然(递归要每层传递)、调试时一个断点就能捕获所有状态转换(递归分散在多个栈帧)。

2. Schema 驱动而非硬编码。 每个工具用 Zod Schema 定义输入,验证逻辑、权限检查、API 描述都从同一个 Schema 派生。这就是”单一真相源”——参数改一处,全局生效,消除了三处手动同步的不一致隐患。

3. 渐进式权限。 权限分阶段短路检查(见配置篇的四阶段管线)。好处是”早期拒绝”

,权限明确拒绝就不必弹用户确认。每一阶段都是一层筛子。

4. 流式优先。 从模型响应到工具结果,所有数据通过 AsyncGeneratoryield 传递,消费端逐条处理而非等整回合结束。这是实时 UI 和 SDK 流式消费的基础。

5. 可插拔扩展。 在生命周期节点提供钩子扩展点,每个钩子是独立的 Shell 命令或 HTTP 端点,通过标准化 JSON 协议交互。核心引擎不需要知道扩展逻辑的存在。

这五条不是孤立技巧,而是相互支撑的决策网络。当你在自己的项目里遇到本专题没覆盖的场景时,回到这五条,它们能帮你做出和整体一致的选择。

三、六步路线图

搭 Harness 是渐进过程。下面六步,每步建立在前一步之上,最终产出一个最小可运行的 Harness:

flowchart TD
    S1["Step 1: 对话循环<br/>AsyncGenerator + while(true)"] --> S2["Step 2: 工具系统<br/>buildTool 工厂函数"]
    S2 --> S3["Step 3: 权限管线<br/>四阶段短路检查"]
    S3 --> S4["Step 4: 上下文管理<br/>渐进式压缩策略"]
    S4 --> S5["Step 5: 记忆系统<br/>提取/存储/注入"]
    S5 --> S6["Step 6: 钩子系统<br/>生命周期扩展点"]
    S6 --> DONE["最小可运行 Harness"]

    classDef stepNode fill:#e8f4f8,stroke:#2980b9,stroke-width:1px
    classDef doneNode fill:#d4edda,stroke:#27ae60,stroke-width:2px
    class S1,S2,S3,S4,S5,S6 stepNode
    class DONE doneNode

别被”六步”迷惑,这不是瀑布流程。实践中你会在步骤之间反复迭代

,实现上下文管理时可能调整对话循环的状态结构。六步的意义是提供有序的学习路径,不是僵化的开发顺序。

Step 1
——心脏

对话循环是 Harness 的心脏。用 AsyncGenerator 实现一个”解构状态 → 调 API → 检测工具调用 → 执行 → 回填消息 → 决定继续或终止”的循环。核心骨架:

async function* query(initialState: State): AsyncGenerator<StreamEvent> {
  let state = initialState
  while (true) {
    // 0. 中止检查(循环顶部,最自然的退出点)
    if (state.signal.aborted) return

    // 1. 预处理:上下文过大则压缩(Step 4)
    state = await maybeCompact(state)

    // 2. 调用 LLM,流式接收
    const response = streamLLM(state.messages, state.tools)
    let toolCalls = []
    for await (const chunk of response) {
      yield { type: 'text', delta: chunk.text }   // 增量输出给 UI
      if (chunk.toolCall) toolCalls.push(chunk.toolCall)
    }

    // 3. 没有工具调用 → 纯文本回复,终止
    if (toolCalls.length === 0) return

    // 4. 执行工具(经过权限管线 Step 3),结果回填
    const results = await executeTools(toolCalls, state)  // yield 进度
    state = { ...state, messages: [...state.messages, ...results] }
    // 5. continue —— 带着新消息进入下一轮
  }
}

关键在于状态是不可变

{ ...state, ... } 整体替换,而非逐字段修改。这让每次状态变化都可追溯,也让 Step 4 的压缩变成”替换 messages + continue”这么简单的一件事。

Step 2
——双手

每个工具用一个 buildTool 工厂从 Schema 派生完整行为。工具契约至少包含

、Zod 输入 Schema、checkPermissions(工具特定的权限逻辑)、execute(实际执行,支持进度回调)、以及 readOnly/destructive/concurrencySafe 三个元数据(决定调度和权限)。Schema 驱动确保验证、权限、文档同源。

Step 3
——护栏

四阶段短路检查:validateInput(Zod 校验) → 规则匹配(deny > ask > allow 铁律) → checkPermissions(上下文评估) → 交互式确认。核心是 deny 永远压过 allow,以及默认值必须 fail-closed(校验失败、回调抛错都降级为”更保守”而非”更激进”)。细节见配置篇

Step 4
——工作记忆

有效上下文窗口是有限的。渐进式压缩

(裁剪工具结果、折叠旧消息),不够再上 LLM 摘要。务必加熔断——连续压缩失败达到上限(比如 3 次)就停,别陷入”压缩失败→重试→再失败”的死循环。

Step 5
——长期记忆

跨会话的持久记忆。核心原则是”只保存无法推导的信息”

、git 历史、能重新读出来的东西都不该进记忆;用户偏好、项目约定、踩过的坑才值得存。用一个索引文件(如 MEMORY.md)做目录,按需加载具体条目,避免每次把全部记忆塞进上下文。

Step 6
——神经末梢

在生命周期节点(SessionStart、PreToolUse、PostToolUse、Stop 等)提供扩展点。每个钩子是独立 Shell 命令,通过 JSON 输入输出与 Harness 交互。这是”确定性控制”的关键——有些规则不能靠模型自觉,必须用钩子强制(比如提交前必须测试通过)。

走完这六步,你就有了一个能对话、能调工具、有安全护栏、会管上下文、有记忆、可扩展的最小 Harness。剩下的都是在这个骨架上做加法。

四、运行时选型

Claude Code 选 Bun,主要图三点

(约 Node 的三分之一,对 CLI 很关键)、原生 TypeScript(无需预编译)、编译时特性开关(构建阶段消除不用的代码路径)。但这不是唯一解:

维度BunNode.jsPython
启动速度极快中等
TypeScript 原生需编译
包生态成熟度中等极高极高
特性开关支持内置需工具需工具
部署普遍性新兴广泛广泛
类型安全

如果你已有 Node.js 基础设施,Node 完全可行;数据科学/ML 场景 Python 更顺,只是类型安全和工具生态弱一些。选型没有标准答案,取决于你的部署形态——这正是对比篇的核心结论

的差异都是同一批原则在不同部署形态下的折中。

五、落地清单
从零到开箱即用

理论讲完,回到最实际的问题。很多人卡在”环境都搭不起来”这一步。下面是 Windows 上的完整落地路径,按依赖顺序排列。

基础环境(必装,四步):

  1. PowerShell 7.x —— Win10 自带的是 5.x,补全和体验差一截。winget install --id Microsoft.PowerShell,装完用 pwsh 启动(和 5.x 是独立程序)。
  2. Node.js —— 后续几乎所有安装都依赖它。用官网 msi 安装包(自动配环境变量),装完 node -v / npm -v / npx -v 都能用。
  3. Git —— 版本管理是安全底线。AI 有预期外改动时,git diff 和分支能救命。CLAUDE.md 里务必写清”不主动提交/推送、不 —force 到 main”。
  4. Claude Code —— irm https://claude.ai/install.ps1 | iex,然后 claude 进入。首次要选登录方式(官方账号 / API Key / 第三方中转)。

网络提示

API 需要代理。会话级临时设置:$env:https_proxy="http://127.0.0.1:7890"。别嫌麻烦,这比”永久生效”更可控。

进阶扩展(按需选装):

  • cc-switch —— 图形化管理多个供应商配置(官方 / 中转),一键切换,还能多端同步 MCP、Skill、API 配置。新手强烈推荐,省去手改 settings.json 的痛苦。
  • MCP Server —— 不用一上来装一堆。真正常用的就几个
    (库文档检索,解决知识陈旧)、DeepWiki(GitHub 仓库 AI 文档)、Playwright(网页自动化测试)。用一段时间知道自己缺什么了再补。
  • 状态栏增强(如 CCometixLine) —— 实时看上下文窗口用量,配合 /context 命令,对管理 token 很有帮助。
  • Workflow 脚手架 —— 如果要做大型复杂项目,可以引入规范驱动的开发工具(OpenSpec 这类),或社区的 workflow 方案,给流程加骨架。

如果你连这些都不想手动配,社区里有像 CCQ(Claude Code Quickstart)这样的一键安装脚本,把上面基础环境四步 + 进阶扩展做成了交互式选装,凭据还能持久化到本地 vault,重装不用重填。适合”想体验但不想折腾环境”的人。

但记住第二篇说过的话: 你只要会安装、启动、对话就足够开始了。上面的扩展全是非必要的。用顺手了、知道自己需求了,再回头看这些功能,你很快就明白它们的场景。一开始就想学全,只会消磨掉学习的动力。

结语
是产品,不是脚手架

整个专题拆了这么多,最后想留下的就一句话——也是对比篇里 Claude Code 和 Codex 殊途同归的那句:

Harness 承担不变量,模型承担决策。

差异化的产品力,从来不来自”你调用哪个 LLM”——这部分谁都能调到最好的模型。差异化来自 harness 怎么把那个 LLM 卡进自己的工程不变量层

,哪些留给模型;哪些默认值保守兜底,哪些边界用类型或内核钉死。

读懂 Claude Code 的设计,不是为了复刻它,而是为了在你自己的场景里,知道每一个”为什么这样设计”背后的权衡。当你能对自己的每个设计决策说清”如果不这样会怎样”,你就真正拥有了这套可迁移的心智模型。

这个专题到此结束。如果它帮你把”会用 Claude Code”往前推了一步,变成”看得懂、也能自己搭”,那就够了。


本篇主要参考:

  • 《御舆
    Agent Harness》第 15 章”构建你自己的 Agent Harness”(lintsinghua/claude-code-book,CC BY-NC-SA 4.0)
  • CCQ(Claude Code Quickstart)项目介绍 —— Windows 环境搭建清单
  • 本专题第一篇第七篇