智能体 Harness:把 AI Agent 接入工程工作流

2026年9月26日

智能体 Harness:把 AI Agent 接入工程工作流

ChatGPT 刚火起来的时候,我的使用方式是打开浏览器,把代码片段贴进去,等它给建议,再手动复制回 IDE。这种模式叫「聊天式辅助」—— AI 只管说,不动手。真正改变效率曲线的是 Agent:它不只是给答案,而是直接读文件、跑命令、改代码、提交 PR。问题是,把一个大语言模型放进能操作文件系统的环境里,相当于给一个没有身体感知的小孩一把螺丝刀。它可以修东西,也可以把主板捅穿。

Harness 就是这把螺丝刀的「安全手柄」。它不是 Agent 本身,而是 Agent 与真实工程环境之间的中间层:决定 Agent 能调用哪些工具、能看到什么上下文、执行结果如何回传,以及出了错怎么兜底。本文想聊的是,怎么设计一个靠谱的 Harness,让 Agent 真正融入日常开发,而不是成为需要人盯着的麻烦。

核心问题:Agent 能力很好,但怎么让它安全地操作代码?

Agent 的能力边界在快速扩展。从早期的文本补全,到 GitHub Copilot 的 inline suggestion,再到 Cursor Composer、Pi Coding Agent 这类能跨文件改代码的系统,AI 正在从「副驾驶」变成「轮班工程师」。但能力越大,风险越具体:

  • 幻觉导致破坏:LLM 可能自信地删除看起来「没用」的代码,而那恰好是兼容旧版本的 fallback。
  • 权限过大:如果 Agent 拿到了 shell 和 git 权限,一次误判就能 git push --force 到主分支。
  • 上下文错配:Agent 看不到 CI 失败日志,却基于过时的本地状态给出修复建议。
  • 不可审计:手动操作至少有人记得改了什么,Agent 半夜跑了 20 个步骤,第二天没人说得清。

一句话:Agent 需要工程环境的能力,但工程环境不能毫无保留地信任 Agent。 Harness 就是这个信任交换的协议层。

Harness 是什么:不只是「工具调用」

先说清楚 Harness 不是什么。OpenAI 的 Function Calling、Anthropic 的 Tool Use、以及社区里的 MCP(Model Context Protocol),本质都是定义一套 Schema,让模型学会在什么时候调用外部函数。它们解决的是「模型怎么请求工具」的问题。

Harness 解决的是谁来执行、怎么准备环境、怎么保证安全、怎么把结果喂回去的问题。打个比方:

  • Function Calling 是「餐厅菜单」—— 告诉模型有哪些菜可以点。
  • Harness 是「后厨 + 服务员 + 收银系统」—— 接单、备料、烹饪、上菜、处理退单。

在 Pi Coding Agent 这类系统里,Harness 通常表现为一个本地守护进程(或者嵌入在 IDE 插件里的进程)。它向 Agent 暴露一组能力声明(Capabilities),Agent 根据当前任务选择调用。Harness 收到请求后,做四件事:

  1. 能力发现(Discovery):告诉 Agent 当前环境有哪些工具可用。比如在一个没有 Docker 的机器上,就不应该暴露 docker_build 这个工具。
  2. 上下文注入(Context Injection):把 Agent 需要的背景信息打包进请求。不是把整个项目目录 dump 过去,而是按需读取相关文件、最近修改、测试失败摘要。
  3. 执行沙箱(Sandboxed Execution):在受控环境里跑命令或写文件。沙箱可以是 chroot、容器、或者最简单的白名单校验。
  4. 结果回传(Result Feedback):把 stdout、stderr、文件 diff、HTTP 响应格式化后返回给 Agent,通常附带结构化的元数据(退出码、耗时、影响文件列表)。

Function Calling 和 MCP 是协议,Harness 是运行时。两者配合:Agent 用协议表达意图,Harness 在运行时把它落地。

Harness 架构:Agent 通过 Harness 与工程环境交互

图:Harness 作为中间层,向上暴露能力声明,向下管控对文件系统、Shell 和网络的访问。

一个最小 Harness:从文件读写到命令执行

为了把概念落地,我设想一个最简 Harness,只有两个工具:read_file 和 exec_command。Agent 想修改项目里的 src/config.ts,流程如下:

一次工具调用的典型流程

vbnet
Agent: read_file("src/config.ts")
Harness: 校验路径是否在项目根目录内 → 读取内容 → 返回

Agent: exec_command("git diff")
Harness: 校验命令在白名单内 → 在仓库根目录执行 → 捕获 stdout/stderr → 返回

下面是一个粗糙但可用的 Harness 骨架(TypeScript 伪代码):

typescript
interface ToolRequest {
  name: 'read_file' | 'exec_command';
  args: Record<string, string>;
}

class MiniHarness {
  private rootDir: string;
  private allowedCommands = new Set(['git', 'npm', 'node', 'cat']);

  async dispatch(req: ToolRequest) {
    switch (req.name) {
      case 'read_file':
        return this.safeRead(req.args.path);
      case 'exec_command':
        return this.safeExec(req.args.cmd);
    }
  }

  private safeRead(rawPath: string) {
    const resolved = path.resolve(this.rootDir, rawPath);
    // 路径逃逸检查
    if (!resolved.startsWith(this.rootDir)) {
      throw new Error('Path escape detected');
    }
    return fs.readFileSync(resolved, 'utf-8');
  }

  private safeExec(rawCmd: string) {
    const [bin, ...args] = rawCmd.split(' ');
    if (!this.allowedCommands.has(bin)) {
      throw new Error('Command not allowed');
    }
    return execFileSync(bin, args, { cwd: this.rootDir, encoding: 'utf-8' });
  }
}

这个骨架能跑,但生产环境马上会踩坑:

  • 路径逃逸:../etc/passwd 被 startsWith 挡住,但符号链接可以绕过去。稳妥做法是先 realpath 再比较。
  • 命令注入:上面用 split(' ') 解析命令是错的。git log --format="%H %s" 里的空格会把参数切碎。应该用数组传参(execFile 而非 exec)。
  • 并发写冲突:两个 Agent 线程同时修改同一个文件,后提交的会覆盖前者。Harness 要么加文件锁,要么把写操作串行化。
  • 副作用不可控:Agent 调用 npm install 改了 node_modules, Harness 如果没有快照机制,回滚时只能 git checkout .,代价很大。

上下文窗口与「工具协议」设计

Agent 每次做决策,依赖的是 Harness 喂给它的上下文。上下文给多了浪费 token,给少了 Agent 瞎猜。Harness 的职责之一就是做信息的策展人(Curator)。

以代码修 bug 为例,理想的信息流应该是:

阶段 Harness 提供的上下文 为什么不是全部
定位 报错堆栈 + 相关文件路径 不需要整个仓库的 AST
理解 报错函数及其调用链的源码 不需要 node_modules
修复 目标文件完整内容 + 依赖接口签名 不需要测试文件全文
验证 测试命令输出(失败用例摘要) 不需要 CI 完整日志

Harness 实现这种分层,通常依赖几种机制:

1. 符号索引(Symbol Index) 预先构建文件 → 符号 → 引用的索引(比如用 Tree-sitter 或 LSP)。Agent 提到 UserService.authenticate,Harness 能快速找到定义位置和调用点,而不是让 Agent 在全仓库里 grep。

2. 最近变更优先 Agent 刚修改过的文件、当前打开的编辑器标签页、最近的 git diff,这些「热数据」命中概率最高,应该优先注入上下文。

3. 结构化摘要 不要把 10 MB 的日志全文塞给 Agent。Harness 可以先做一层预处理:提取报错行、去重堆栈、统计失败频率,再生成一段结构化摘要:

json
{
  "test_suite": "auth.spec.ts",
  "failed_count": 3,
  "first_failure": "UserService.authenticate throws TypeError",
  "stack_top": "src/services/user.ts:42"
}

4. 动态上下文协议 Agent 和 Harness 可以约定一种「按需拉取」的协议。Agent 先拿到骨架,发现缺某个接口定义,再显式请求 read_symbol("PaymentGateway.charge")。这比一次性 dump 所有内容更可控。

关键是:Harness 比 Agent 更了解工程环境的结构,因此应该由 Harness 决定「什么值得看」,而不是让 Agent 自己翻仓库。

安全边界:最小权限与可审计

前面提到了路径逃逸和命令注入,但 Harness 的安全设计不止于输入校验。它是一个纵深防御体系:

1. 文件系统沙箱 最轻量的做法是用 chroot 或绑定挂载把 Agent 限制在项目目录。更现代的做法是在容器里跑 Harness(比如 Docker 的 rootless 模式),配合只读卷挂载。关键是默认拒绝:Agent 想写文件?先问过白名单。

2. 命令白名单与参数校验 不要只校验命令名,还要校验参数模式。比如允许 git diff,但不允许 git push;允许 npm test,但不允许 npm publish。正则或预定义模板都可以:

typescript
const rules = [
  { cmd: 'git', args: /^diff\s/ },
  { cmd: 'npm', args: /^run\s(test|lint|build)$/ },
];

3. 网络隔离 Agent 理论上不应该随意访问外网。但有些任务确实需要(比如查 npm 包版本)。Harness 可以维护一个允许列表(registry.npmjs.org, pypi.org),其余请求默认阻断。如果 Agent 需要搜索,Harness 自己代理请求,再把脱敏后的结果返回。

4. 操作审计 每一次工具调用都应该留下结构化日志,至少包含:

json
{
  "timestamp": "2025-07-15T09:23:17Z",
  "agent_id": "pi-session-42",
  "tool": "write_file",
  "args": { "path": "src/config.ts" },
  "diff_hash": "sha256:a1b2c3...",
  "exit_code": 0
}

有了审计日志,才能在事后回答「Agent 到底改了什么」。更进一步,Harness 可以在执行敏感操作前要求人类确认(比如 git push、rm -rf),把 Agent 的自主性限制在「建议」层面,而非「执行」层面。

5. 回滚能力 Agent 改坏了代码怎么办?Harness 可以在每次写操作前自动 git stash 或打临时分支。如果 Agent 连续多步修改后整体失败,Harness 能一次性回滚到起点,而不是让用户手动 git checkout 20 个文件。

小结

把 AI Agent 接入工程工作流,不能只靠模型能力的提升,还需要一个可靠的 Harness 来做「安全手柄」。几个核心观点:

  1. Harness 是运行时,不是协议。Function Calling / MCP 定义了 Agent 怎么表达意图,Harness 负责在真实环境里安全地执行和回传结果。
  2. 上下文应该由 Harness 策展。Agent 不该自己翻仓库,Harness 要根据任务阶段,按需注入符号、摘要和动态数据,控制 token 消耗和信息精度。
  3. 安全是默认拒绝 + 可审计。沙箱、白名单、网络隔离是基础,操作日志和回滚能力才是让人敢把 Agent 放进 CI 的底气。

如果你正在做一个 IDE 插件、一个 CI Agent、或者一个内部 DevOps 机器人, Harness 的设计质量直接决定了「AI 辅助」和「AI 自动化」之间的鸿沟能不能跨过去。

© 2013 – 2025 陈祥