智能体 Harness:把 AI Agent 接入工程工作流
智能体 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 收到请求后,做四件事:
- 能力发现(Discovery):告诉 Agent 当前环境有哪些工具可用。比如在一个没有 Docker 的机器上,就不应该暴露
docker_build这个工具。 - 上下文注入(Context Injection):把 Agent 需要的背景信息打包进请求。不是把整个项目目录 dump 过去,而是按需读取相关文件、最近修改、测试失败摘要。
- 执行沙箱(Sandboxed Execution):在受控环境里跑命令或写文件。沙箱可以是 chroot、容器、或者最简单的白名单校验。
- 结果回传(Result Feedback):把 stdout、stderr、文件 diff、HTTP 响应格式化后返回给 Agent,通常附带结构化的元数据(退出码、耗时、影响文件列表)。
Function Calling 和 MCP 是协议,Harness 是运行时。两者配合:Agent 用协议表达意图,Harness 在运行时把它落地。
图:Harness 作为中间层,向上暴露能力声明,向下管控对文件系统、Shell 和网络的访问。
一个最小 Harness:从文件读写到命令执行
为了把概念落地,我设想一个最简 Harness,只有两个工具:read_file 和 exec_command。Agent 想修改项目里的 src/config.ts,流程如下:
Agent: read_file("src/config.ts")
Harness: 校验路径是否在项目根目录内 → 读取内容 → 返回
Agent: exec_command("git diff")
Harness: 校验命令在白名单内 → 在仓库根目录执行 → 捕获 stdout/stderr → 返回
下面是一个粗糙但可用的 Harness 骨架(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 可以先做一层预处理:提取报错行、去重堆栈、统计失败频率,再生成一段结构化摘要:
{
"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。正则或预定义模板都可以:
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. 操作审计 每一次工具调用都应该留下结构化日志,至少包含:
{
"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 来做「安全手柄」。几个核心观点:
- Harness 是运行时,不是协议。Function Calling / MCP 定义了 Agent 怎么表达意图,Harness 负责在真实环境里安全地执行和回传结果。
- 上下文应该由 Harness 策展。Agent 不该自己翻仓库,Harness 要根据任务阶段,按需注入符号、摘要和动态数据,控制 token 消耗和信息精度。
- 安全是默认拒绝 + 可审计。沙箱、白名单、网络隔离是基础,操作日志和回滚能力才是让人敢把 Agent 放进 CI 的底气。
如果你正在做一个 IDE 插件、一个 CI Agent、或者一个内部 DevOps 机器人, Harness 的设计质量直接决定了「AI 辅助」和「AI 自动化」之间的鸿沟能不能跨过去。