最近社区里被聊得最多的一个现象是:AI 编码工具越来越强,代码生成已经不是瓶颈,让代码生成过程真正可控反而成了瓶颈。
一个很常见的场景是:你让 AI 助手帮你写一个“读取目录下 CSV 并生成统计报告”的脚本,它很快就给出了一段可运行的 Python 代码,本地测试也没有问题。但合并到主干后,却发现这个脚本在一个不相关的环境里被调度执行了 200 次。原因不是模型写错了代码,而是它运行在一个几乎没有行动边界、没有权限约束、没有审计记录的“自由环境”里。
这个问题不是靠换一个更大的模型能解决的。真正解决问题的是 Harness Engineering,也就是给模型戴上缰绳、划定边界、装好仪表盘和行车记录仪的系统工程。最近 DeepSeek Harness、Codex Harness、Agent Harness 等方向讨论热度很高,很多读者私信问:Harness 是不是又一个 Agent 框架?它和 Agent 到底什么关系?中小团队怎么用 Harness 来保证生成代码可控?
这篇文章会分三个层次展开:
- 先讲清楚概念:Harness Engineering 是什么,它和 Agent 的核心区别在哪里。
- 再拆解实践:生成可控代码必须做好的几类工程手段,包括工具白名单、工作区隔离、沙箱执行、结构化输出、日志审计、插件扩展。
- 最后给出一套最小可运行的 Harness 原型示例,以及本地部署、插件开发和排错思路。
看完这篇文章,你会对“如何约束 AI 代码生成”形成一套可执行的框架,而不是只知道几个概念名词。
1. Harness Engineering 到底是什么
“Harness” 的英文原意是马具、缰绳,也常被翻译为“安全带”或“线束”。在 LLM 应用工程里,它指的是围绕模型推理循环建立的一整套外部工程装置。
单纯从模型视角看,一个 LLM 的完整工作过程是:用户输入 Prompt,模型结合上下文内容,逐 Token 生成输出。这个循环本身非常简单,复杂的是外部系统怎么控制它、约束它、观测它。Harness 就是连接模型和真实世界的中间层,它通常包含:
- 工具调用注册与白名单管理。
- 工作区与文件访问限制。
- 命令执行时的沙箱隔离。
- 模型输出格式校验与安全过滤。
- 完整调用链路的审计日志。
- 插件扩展机制。
为什么要加这一层?我们可以对比两个流程。
没有 Harness 时的典型流程是:
用户 Prompt -> 模型直接调用工具 -> 生成代码 -> 结果交给使用者这个流程的问题很明显:模型一旦产生工具调用,所有的系统权限就等于交给了模型。模型读到的上下文如果被污染,或者模型本身产生了幻觉,就可能调用一个它根本不该调用的接口。而且这个过程中没有完整的日志,出了事故也无法追踪。
有 Harness 之后的流程是:
用户 Prompt -> Harness 解析意图 -> 查询允许的操作范围 -> 模型在约束内调用工具 -> 输出校验 -> 审计日志 -> 回滚机制同样是生成代码,多了一个中间层之后,模型能看到什么、能调用什么、能在哪一层沙箱中执行,全部由工程策略决定,而不是由模型自由决定。
所以我认为 Harness 工程的核心判断可以概括为:让代码生成可控的关键,不在于找更强的模型,而在于把模型放进一个设计过边界和安全策略的 Harness 中。2026 年再做 Agent 相关项目,如果只关注模型能力而忽略 Harness 建设,大概率会在上线后遇到权限失控、审计缺失和回滚困难的问题。
2. Agent 与 Harness:一个管“聪明”,一个管“听话”
很多读者会把 Agent 和 Harness 搞混,原因也正常。因为你在使用一些开源 Agent 框架时,会看到配置里既有工具、又有记忆、又有执行器,看起来像是一个完整的“指挥系统”,大家就把这套系统直接叫 Agent。
但从工程视角划分,两者关注的问题完全不同。
| 对比维度 | Agent | Harness |
|---|---|---|
| 核心问题 | 让模型能自主规划、选择工具、反思改进 | 让模型只能在允许范围内规划和执行 |
| 关注点 | 任务分解、推理策略、工具选择算法 | 工具约束、沙箱边界、日志审计、输出校验 |
| 抽象层级 | 算法与行为层 | 工程与基础设施层 |
| 典型机制 | ReAct 循环、Plan-and-Execute、反思 | 白名单、工作区、Schema 校验、审计追踪 |
| 负责方向 | 提升任务完成质量 | 降低不可控风险 |
一个常见的类比是:Agent 是驾驶员,Harness 是道路安全系统。驾驶员再厉害,也需要红绿灯、车道线、限速牌和行车记录仪。反过来,一个驾驶技术一般的驾驶员,只要道路规则明确,事故概率也会大幅下降。
在真实开发里,这个区别会直接影响排错方向。如果你的 Agent 经常选错工具,那是推理策略问题,需要调整 Agent 的规划逻辑。如果你的 Agent 总是调用到危险工具、读取到不该读取的文件、或者产生没有日志的意外操作,那不是模型不够聪明,而是 Harness 没设计好。
从团队协作角度看,两者也应该由不同角色关注:算法工程师可能更关注 Agent 的推理效果,而平台工程师需要关注 Harness 的可靠性、可观测性和安全边界。很多中小团队一开始只有一个人兼职做这两件事,所以更容易混淆。但只要把概念边界搞清楚,后续的配置、排错和扩展才会有正确方向。
3. 可控代码生成的五大核心原则
要把 Harness 真正落地到代码生成场景,下面五个原则是必须考虑的。它们不是可选的优化项,而是可控性的底线。
3.1 原则一:限制行动空间
模型的动作空间应该是一个“最小集合”。凡是没显式允许的工具调用,默认都禁止;凡是不需要的文件系统路径,默认都不可访问;凡是不能确定安全性的网络请求,默认都拦截。
落地提示:把所有工具调用收敛到注册表里,显式列出 allow 和 deny 列表。不要让模型直接执行任意 Shell 命令,除非你确认这条命令只能在沙箱里运行。
3.2 原则二:隔离工作区
AI 生成代码时应该只读写一个被严格隔离的工作区目录。密钥、生产配置、数据库连接串、其他项目的代码,都不应该出现在模型的上下文和可用路径里。
落地提示:在 Harness 层做路径解析与校验,任何文件读写都必须经过工作区路径检查。发现目标路径跳出工作区时,直接拒绝操作。
3.3 原则三:强制输出验证
模型生成代码后,不能直接当作有效代码使用。要先做格式校验、Schema 校验、静态检查、危险模式识别,再决定是否进入下一步流程。
落地提示:输出验证可以拆成多层:结构层看是否符合格式,内容层看是否包含危险表达式,行为层看是否调用了不合法接口,最后再跑测试套件。
3.4 原则四:全程可观测
每一次模型输入、每一次工具调用、每一个生成结果,都必须记录进审计日志。没有日志就没有排查依据,没有日志就无法回答“为什么模型会做这个操作”这个最基本的问题。
落地提示:日志要包含调用时间、调用方、工具名、参数摘要、调用结果、耗时、错误信息。生产环境里日志需要集中管理和定期归档。
3.5 原则五:可灰度可回滚
AI 生成的代码不应该直接进入生产环境。它需要先落到评审分支,经过静态扫描、单元测试、人工审核,才能合并。一旦发现问题,能够快速回滚到上一个稳定版本。
落地提示:在 Harness 设计阶段就预留快照和回滚接口,否则等出了事故再补,就来不及了。
4. 六种实用的 Harness 落地实践
原则要落地,必须变成具体的工程实践。下面这六种做法,是当前 Harness 工程里最常用、也是和“生成可控代码”关系最紧密的六个方向。
4.1 工具注册表与白名单机制
避免让模型直接调用系统命令,这是可控性的起点。所有模型可调用的工具,都应该在 Harness 启动时注册进一个工具注册表。注册表里需要包含工具名、参数 Schema、执行入口、权限要求、白名单标记、审计策略。
具体做法是:
- 列出模型在任务中可能需要的所有操作,例如读取文件、列出目录、运行测试、搜索代码。
- 逐项判断:这个操作是否必要?是否可以用更小权限的替代方案?
- 把允许的操作加入白名单,把可能造成破坏的操作加入拒绝列表。
- 运行时在 call_tool 入口统一做一次检查,绕过白名单的调用直接拒绝。
这层机制解决的是模型“能做什么”的问题。如果工具注册表设计得好,模型无法访问白名单之外的任何能力,天然就能挡住大量随意操作。
4.2 工作区与会话隔离
AI 编码工具通常需要一个工作区,用来存放生成的文件、临时文件、测试产物。Harness 需要保证:模型只能在这个工作区内部写文件,不能跳出去写别的目录。
具体实现时,可以在文件读写接口上加一层路径解析函数,把相对路径转换成绝对路径,再检查转换后的路径是否位于工作区根目录之下。所有结果路径必须经过这层校验,否则拒绝操作。
会话隔离则是指:不同任务、不同用户、不同项目的上下文与文件不能互相干扰。一个会话中生成的文件,另一个会话默认不可见。这样能避免模型在并发的多个任务中串号,也能减少上下文污染带来的错误输入。
4.3 沙箱执行
如果模型需要执行命令,比如运行测试或编译代码,那么命令执行必须发生在沙箱内。沙箱的技术选型可以包括容器运行时、用户态隔离、进程资源限制等。
这里有三个重点:
- 不要给沙箱挂载宿主机的真实敏感目录。
- 不要把宿主机的 Docker Socket 挂载给 Agent,否则 Agent 拥有创建特权容器的能力,等于突破了隔离边界。
- 沙箱内的网络权限也需要控制,尽量只放行白名单域名。
沙箱的意义是:即使模型生成的代码被执行了,它也只能影响沙箱内部,无法扩散到真实系统。
4.4 结构化输出与 Schema 校验
很多模型调用失败的案例,不是模型能力不足,而是输出格式不稳定导致的。Harness 应该在系统层面强制模型输出结构化结果,然后进行一次严格的 Schema 校验。
常见做法是:
- Prompt 中明确要求返回 JSON 或 YAML 格式。
- Harness 侧定义生成结果的 JSON Schema,包含字段、类型、必填项约束。
- 模型输出后,先解析为结构体,再做格式与类型校验。
- 校验不通过时,可以进行有限次重试,或者将错误信息反馈给模型修正。
这一步能解决“模型看起来生成了代码,但实际上整个结构是乱的”这类问题。
4.5 日志审计与复盘
审计日志是最容易被忽略、但又是最重要的可控性保障。当前实际项目中,好的审计日志应包括:
- 请求时间与请求 ID。
- Prompt 摘要与完整内容的归档位置。
- 工具调用的参数快照。
- 工具返回的结果摘要。
- 输出校验是否通过以及失败原因。
- 生成代码的去向与版本信息。
有了日志之后,任何一次工具调用异常都可以回放,任何一次生成结果导致的线上问题都能找到原因。这是模型调试和事故复盘的基础。
4.6 插件系统
成熟的 Harness 通常提供插件机制,允许团队在不修改核心代码的前提下,扩展事件钩子。例如,在模型准备调用工具之前做一个自定义安全检查,在生成结果之后自动跑一次团队内部的规范扫描。
插件系统的价值不是增加功能,而是把团队的纪律约束注入到 Harness 的每一个关键节点。你可以在事件总线上挂载自定义动作:
- on_tool_call:工具调用前拦截。
- on_generated:处理模型输出。
- on_error:异常时执行自定义降级策略。
这样,核心 Harness 保持稳定,而团队的个性化规则可以通过插件持续迭代。
5. 最小可运行的 Harness 原型
为了把上面的概念落到实践,下面给出一个最小可运行的 Harness 原型。它不是一个成熟开源项目的官方 API,而是用于学习“约束、工具、校验、日志”这四件事如何组合的最小示例。
5.1 原型设计目标
这个原型要演示三层控制:
- 工具白名单:未被注册的工具无法调用。
- 工作区路径限制:所有文件写入必须位于工作区内。
- 输出校验:包含危险表达式的生成代码会被拒绝。
5.2 目录结构
demo-harness/ ├── demo_harness.py ├── harness_config.yaml ├── schemas/ │ └── generated_code.json ├── plugins/ │ └── example_plugin.py ├── workspace/ └── logs/5.3 配置文件:harness_config.yaml
harness: name: demo-dev-harness workspace: ./workspace log_dir: ./logs tools: allowed: - list_files - exec_command denied: - drop_database - delete_production llm: provider: local_model_or_remote_api temperature: 0.2 max_tokens: 1000 output: require_test_pass: true allow_fuzzy_schema: false这份配置的重点是tools.allowed和tools.denied两个列表。实际项目中,这两个列表通常会由安全团队或项目负责人维护,而不是由普通开发者随意修改。
5.4 核心代码:demo_harness.py
""" demo_harness.py 一个用于教学的最小 Harness 原型。 演示了工具白名单、工作区路径限制、输出内容校验三层控制。 真实生产环境请使用成熟的 Harness 框架,并配合沙箱、审计和权限体系。 """ import argparse import json import os import re from pathlib import Path from typing import Any, Callable, Dict, List try: import yaml except ImportError: raise SystemExit("missing dependency: pip install pyyaml") def load_config(path: str) -> Dict[str, Any]: """从 YAML 文件读取 Harness 配置。""" with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) class Harness: def __init__(self, config: Dict[str, Any]): self.config = config self.workspace = Path(config["harness"]["workspace"]).resolve() self.workspace.mkdir(parents=True, exist_ok=True) self.log_dir = Path(config["harness"]["log_dir"]).resolve() self.log_dir.mkdir(parents=True, exist_ok=True) self._allowed_tools = set(config["tools"]["allowed"]) self._denied_tools = set(config["tools"]["denied"]) self._tool_funcs: Dict[str, Callable] = {} def register_tool(self, name: str, func: Callable) -> None: """只有白名单内的工具允许注册。""" if name in self._denied_tools: raise PermissionError(f"tool {name} is in denied list") if name not in self._allowed_tools: raise PermissionError(f"tool {name} is not in allowed list") self._tool_funcs[name] = func def call_tool(self, name: str, args: Dict[str, Any]) -> Any: """统一工具调用入口,执行白名单检查并记录审计日志。""" if name not in self._allowed_tools or name not in self._tool_funcs: self.audit("denied", {"tool": name, "args": args}) raise PermissionError(f"tool {name} not allowed or not registered") if name in self._denied_tools: self.audit("denied", {"tool": name, "args": args}) raise PermissionError(f"tool {name} is denied") self.audit("call", {"tool": name, "args": args}) return self._tool_funcs[name](**args) def audit(self, action: str, detail: Dict[str, Any]) -> None: """将关键事件写入审计日志。""" record = json.dumps( {"action": action, "detail": detail}, ensure_ascii=False, ) log_file = self.log_dir / "harness.log" with open(log_file, "a", encoding="utf-8") as f: f.write(record + "\n") def resolve_in_workspace(self, filename: str) -> Path: """强制所有文件路径都落在工作区内部。""" target = (self.workspace / filename).resolve() if not target.is_relative_to(self.workspace): raise PermissionError(f"write path {target} is outside workspace") target.parent.mkdir(parents=True, exist_ok=True) return target def validate_output(self, generated_code: str) -> bool: """最小输出校验:拒绝明显危险表达式。""" dangerous_patterns = [ r"subprocess\.Popen\(.*shell=True", r"import\s+os\.system", r"rm\s+-rf\s+/", r"DROP\s+TABLE", ] for pattern in dangerous_patterns: if re.search(pattern, generated_code, re.IGNORECASE): self.audit("rejected", {"reason": "dangerous_pattern", "pattern": pattern}) return False return True def tool_list_files(path: str = ".") -> List[str]: """示例工具:列出目录内容。""" entries = os.listdir(path) return entries[:20] def tool_exec_command(command: str) -> str: """ 示例工具:执行简单命令。 生产环境不要直接这样实现,应该借助沙箱和进程隔离。 """ if "rm" in command or "drop" in command.lower(): raise PermissionError("command rejected by tool-level policy") import subprocess result = subprocess.run( command, shell=True, capture_output=True, text=True, timeout=10 ) return result.stdout[:2000] def main(): parser = argparse.ArgumentParser(description="demo harness") parser.add_argument("--config", default="harness_config.yaml") parser.add_argument("--task", default="list files in workspace") args = parser.parse_args() config = load_config(args.config) harness = Harness(config) # 注册工具 harness.register_tool("list_files", tool_list_files) harness.register_tool("exec_command", tool_exec_command) print("=== demo harness ===") print("task:", args.task) print("allowed tools:", harness._allowed_tools) # 1. 正常工具调用 result = harness.call_tool("list_files", {"path": harness.workspace}) print("tool result:", result) # 2. 模拟一次白名单之外的调用,应该被拒绝 try: harness.call_tool("drop_database", {"sql": "DROP TABLE users"}) except PermissionError as exc: print("denied as expected:", exc) # 3. 模拟一次输出校验:这段代码不会被真正执行,只是验证校验逻辑 generated_code = """ import subprocess subprocess.Popen("dangerous_command", shell=True) """ if not harness.validate_output(generated_code): print("generated code rejected by output validator") else: print("generated code passed") if __name__ == "__main__": main()这个原型的核心逻辑集中在三个地方:
register_tool保证工具必须来自白名单。call_tool做统一入口拦截并记录审计日志。validate_output对生成代码做危险模式过滤。
5.5 插件示例:plugins/example_plugin.py
""" plugins/example_plugin.py 一个简化的 Harness 插件示例,演示事件钩子的扩展方式。 真实插件系统通常基于事件总线,这里只展示核心思路。 """ from typing import Dict class ExamplePlugin: def __init__(self, harness): self.harness = harness def on_tool_call(self, tool_name: str, args: Dict[str, str]): """在工具调用前执行自定义策略。""" self.harness.audit("plugin_tool_call", {"tool": tool_name}) if tool_name == "exec_command": command = args.get("command", "") if "secret" in command.lower() or "env" in command: raise PermissionError("plugin blocked sensitive command") def on_generated(self, code: str) -> str: """对模型生成结果做团队规范检查。""" if len(code) < 10: raise ValueError("generated code is too short, please regenerate") if "TODO" in code: self.harness.audit("plugin_todo_warning", {"message": "generated code contains TODO"}) return code这个插件示例想表达的是:插件机制可以把团队的纪律注入到 Harness 的关键节点,而不需要修改核心代码。
5.6 运行与验证
准备环境:
cd demo-harness pip install pyyaml运行原型:
python demo_harness.py --config harness_config.yaml --task "生成一个 CSV 统计脚本"预期输出大致是:
=== demo harness === task: 生成一个 CSV 统计脚本 allowed tools: {'list_files', 'exec_command'} tool result: [文件名列表...] denied as expected: tool drop_database not allowed or not registered generated code rejected by output validator验证要点有三个:
list_files能正常返回,说明白名单工具可以调用。drop_database被拒绝,说明白名单拦截生效。- 包含危险表达式的生成结果被校验器拒绝,说明输出校验生效。
同时可以在logs/harness.log里查看审计记录:
{"action": "call", "detail": {"tool": "list_files", "args": {"path": "/.../workspace"}}} {"action": "denied", "detail": {"tool": "drop_database", "args": {"sql": "DROP TABLE users"}}} {"action": "rejected", "detail": {"reason": "dangerous_pattern", "pattern": "subprocess\\.Popen\\(.*shell=True"}}这个示例的价值在于,它把 Harness 的最小闭环跑通了一遍:配置、注册、调用、拦截、审计、校验。你可以把这个原型继续扩展成支持更多工具、更多校验规则、更多日志输出的工程原型。
6. 本地部署与插件开发技巧
6.1 为什么很多团队选择本地部署 Harness
数据隐私和安全边界是最主要原因。如果把上下文、代码、内部 API 描述都发送到外部模型服务,很多企业无法接受。本地部署可以做到:
- 模型调用保持在本地网络,敏感数据不出内网。
- 工具执行环境完全由企业控制,方便对接内部系统。
- 可以更精细地做审计,避免第三方平台留存数据。
当然,本地部署也意味着要自己处理模型资源、依赖管理、升级维护等问题。对于还没有完整运维能力的团队,建议先从单机原型开始,不要一上来就搭建整套分布式平台。
6.2 通用部署路径
从社区常见实践看,本地部署 Harness 通常有三条路径:
- 下载预编译产物:适合只想快速体验的场景。
- 源码构建:适合需要深度定制和插件开发的团队。
- Docker 部署:适合需要快速复现环境、方便迁移的场景。
源码构建时,最容易遇到的问题往往不是功能代码,而是依赖版本。尤其是基于 Node.js 生态的项目,不同版本的 pnpm、Node、lockfile 格式之间兼容性差异较大,经常出现在pnpm install或者启动 Web 控制台时卡住的情况。
如果遇到类似“卡在 pnpm dsh web”这样的问题,通用排查顺序是:
- 检查 Node 版本是否满足项目要求。
- 检查包管理器版本是否和 lockfile 匹配。
- 检查依赖源是否可达,必要时切换镜像源。
- 清理缓存和临时目录后重试。
- 尽量避免在生产环境直接使用最新版本,先确认社区内是否已有兼容性报告。
6.3 一个 Docker Compose 部署模板
下面是一个通用的 Docker Compose 模板,它演示的是“Harness 服务 + 工作区 + 日志目录”的部署形态,具体镜像、端口和内容按照你的实际项目替换:
# docker-compose.yml(通用模板,按实际项目调整) services: harness-api: build: . ports: - "8080:8080" volumes: - ./workspace:/app/workspace - ./logs:/app/logs environment: LOG_LEVEL: debug SANDBOX_ENABLED: "true" MAX_CONTEXT_TOKENS: "16000" restart: unless-stopped部署时要注意几个点:
- 工作区目录和日志目录必须挂载持久化卷,否则容器重启后数据丢失。
- 环境变量中的
SANDBOX_ENABLED建议一直保持true。 - 如果 Harness 需要调用本地模型服务,需要通过 extra_hosts 或网络配置保证容器能访问内网模型地址。
6.4 插件开发套路
插件开发通常是团队扩展 Harness 能力的首选方式。通用套路是:
- 找到 Harness 提供的事件钩子文档,确认你要注入的是哪个阶段。
- 编写插件类,实现对应事件方法。
- 在 Harness 配置中声明插件路径,启动时扫描并注册。
- 为插件编写单独的日志前缀,方便与核心日志区分。
- 先在小规模任务上验证,再放开到全量任务。
插件不是越多越好。每增加一个插件,就增加