☰
Harness实践:让大模型代码生成变得可控可校验
2026/10/5 11:43:57 网站建设 项目流程

不知道你有没有碰到过这种场景:让大模型生成一段代码,它给出的结果看起来结构完整、注释齐全,但一旦放进真实项目里,就会出现格式不匹配、语法报错、依赖缺失,甚至整体风格与你团队规范完全不一致。如果只是单次问答,这种问题影响不大;但如果是在自动化代码生成、代码评审、数据管道脚本生成等工程场景里,这就会变成巨大的稳定性隐患。

我最近在推进一个内部代码生成项目时,对这个问题体会很深。项目初期,我们把大量精力花在优化提示词上,反复调整"请你生成一个函数""请使用 Python 编写"之类的描述,结果生成质量依然不稳定。后来团队把视角从"怎么把模型问好"切换成"怎么在模型外面搭一套工程约束框架",事情才开始出现转机。这套约束框架,就是本文要聊的 Harness 实践。

本文会围绕 Harness 的概念、核心原理和可落地的代码生成约束模式展开,并通过三个可运行的 Python 示例,逐步演示如何把一段裸的大模型输出,改造成可控、可校验、可重试的工程化代码。文章适合正在做 AI 应用落地的后端工程师,也适合对 AI 工程化感兴趣、希望把模型能力真正接进业务系统的同学。

1. 背景与核心概念

1.1 为什么生成代码容易,生成"可控"代码很难

大模型生成代码本身并不难,难的是让它在工程环境里稳定产出"符合预期"的代码。这里有一个容易被忽略的事实:大模型本质上是语言模型,它学习的是文本分布,而不是工程约束。它知道"一段 Python 代码大概长什么样",但它并不天然知道你所在项目的目录结构、依赖版本、命名规范、日志规范和异常处理要求。

举个例子,你让模型生成一个 HTTP 接口,它可能给你返回requests.get的实现;但团队标准是使用httpx.AsyncClient。你让它写一个数据库查询,它可能默认使用 SQLAlchemy 的方式;但项目里统一使用的是 MyBatis 或原生psycopg。这类偏好在提示词里写一次两次可以,但没法保证每次生成都稳定遵循。

更麻烦的是,模型输出具有随机性。同一段提示词,多次调用可能得到不同结果。这在对话场景中是"灵活",在生产场景中就是"不稳定"。可控代码的核心诉求,正是通过模型外部的约束机制,把这种随机性压缩到可接受的范围。

1.2 Harness 是什么

Harness 在英文里有"马具、挽具"的含义,引申为"约束装置、控制框架"。在 AI 工程领域,Harness 通常指围绕模型调用构建的一整套工程化约束层,它负责把模型的输入、输出、校验、反馈、重试、记录等环节整合成一条可控链路。

用更通俗的话说:没有 Harness 时,模型是直接裸露在业务代码里的,返回什么我们就用什么;有了 Harness 后,模型的每次输出都会经过 约束 → 校验 → 反馈 → 修正 的闭环,只有满足预设条件的输出才会进入业务系统。

近年来,围绕 DeepSeek、Codex 等模型的社区工具中也出现了大量被命名为 harness 的项目。它们做的事情通常是:封装模型 API、提供外部工具调用能力、约束模型输出格式、执行结果校验与自动修复。这也从一个侧面说明,Harness 已经成为大模型应用从"能跑"走向"可控"的重要工程模式。

1.3 Harness 与 Agent 的区别

很多人会把 Harness 和 Agent 混在一起,实际上两者解决的是不同层面的问题。

Agent 的核心是"自主决策"。它根据目标自己规划步骤、选择工具、决定何时结束,强调模型的推理和行动能力。Harness 的核心是"约束与控制"。它强调的是在模型外部设定边界,让输出结果可预期、可校验。

用我自己的理解来对比:

维度HarnessAgent
关注点输出的正确性与可控性目标的规划与执行
控制权框架主导,模型按预设路径输出模型主导,框架提供工具支持
适用场景代码生成、结构化输出、批量任务自主问答、复杂任务拆解、多工具协作
风险偏好偏向低风险,宁可重试也不放过异常偏向高灵活度,允许探索性路径
对结果的要求强校验,结果需要满足规则或 Schema弱校验,更关注任务完成度

实际工程中,两者并不是互斥的。比较稳妥的做法是先建立 Harness 的约束和校验闭环,再在闭环之上逐步引入 Agent 的自主能力。直接在裸调用上构建 Agent,往往会出现"Agent 看似聪明,但错误难以追踪"的情况。

2. 环境准备与版本说明

在开始写代码之前,先把环境准备说明白。本文所有示例都基于 Python 3.10+ 开发,使用jsonschema库做 JSON Schema 校验。模型调用部分会先以模拟客户端的方式演示,生产环境可以替换成你自己团队内部的模型网关地址或任意模型 API。

2.1 运行环境

  • 操作系统:Windows 10/11、macOS、Linux 均可。
  • Python 版本:3.10 或以上。示例代码中使用了list[dict]这类类型注解语法,低版本需要改成List[dict]。
  • 包管理工具:pip 或 poetry 均可。

如果你使用的是 Python 3.8 或 3.9,需要把类型注解调整为兼容写法,同时安装from __future__ import annotations。建议直接使用 Python 3.10+,代码更简洁。

2.2 依赖安装

创建一个新的虚拟环境,并安装以下依赖:

python -m venv .venv source .venv/bin/activate # Windows 下使用 .venv\Scripts\activate pip install jsonschema

jsonschema库用于校验模型返回的 JSON 是否符合结构要求。如果你只是做代码语法校验,Python 内置的ast模块就够用了,不需要额外安装依赖。

2.3 示例项目结构

为了便于阅读,这里规划一个简单的项目结构:

harness_practice/ ├── harness_mini.py # 实战一:最小校验型 Harness ├── harness_json.py # 实战二:结构化输出 Harness ├── harness_execute.py # 实战三:可执行代码沙箱 Harness └── common.py # 公共工具函数

下面三个实战案例会对应这三个文件,建议按顺序看完,因为后一个案例会复用前一个案例中的函数。

3. 可控代码的核心链路拆解

在写具体代码之前,先拆解 Harness 的核心链路。理解了这条链路,后面再去看代码就会轻松很多。

一套典型的代码生成 Harness,至少包含三个环节:约束输入、校验输出、反馈闭环。

3.1 约束输入:提示词与输出格式设计

很多人以为提示词只是给模型讲清楚"要做什么",但在 Harness 里,提示词还承担着"输出格式约束"的作用。

我比较推荐的做法是在提示词中明确要求模型使用 Markdown 代码块包裹代码,并且不允许输出解释性文字。这样便于后续用正则表达式把代码提取出来。更严格的场景还可以要求模型输出 JSON,并给出 JSON Schema 作为参考。

举个例子:

你是一个严谨的 Python 代码生成助手。 必须输出如下格式: ```python <完整可运行的 Python 代码>

不允许输出任何多余解释。

这种提示词写法看起来简单,但能显著减少后续解析的麻烦。与之配套的是,在提示词中尽量给出具体需求背景、输入输出示例、约束条件,而不是只丢一句话给模型。 ### 3.2 校验输出:语法、Schema、静态规则 模型给出输出后,Harness 不能直接信任,必须做程序化校验。校验的层级由浅到深可以分为三种: - 格式校验:模型输出是否为合法的代码块或 JSON。 - 语法校验:代码能否通过 AST 解析或编译器检查。 - 语义校验:代码是否满足业务规则,比如函数名是否符合规范、是否引用了不允许的依赖、是否包含危险操作。 格式校验通常用正则或 JSON 解析库;语法校验用 Python 的 `ast` 模块就能实现;语义校验属于工程定制部分,需要根据团队规范编写规则。 ### 3.3 反馈闭环:错误信息回填与重试策略 校验失败后,不能直接把失败结果丢掉,而是要把失败原因以结构化方式反馈给模型,让模型在下一轮生成中修正。 这也是 Harness 中"闭环"的关键含义。比如语法错误发生在第 3 行,就把"第 3 行存在 SyntaxError"作为新提示词的一部分传给模型;JSON 缺少字段,就把缺失字段告诉我模型,并要求其重新输出。 重试次数必须有限制。我通常设置为 3 次到 5 次。超过次数后直接抛出异常,防止模型无限循环消耗资源。重试失败不算 bug,而是一个应该被记录和告警的业务事件。 ## 4. 实战一:最小校验型 Harness 原型 先从最简单的原型开始。这个 Harness 可以完成三件事:提取模型输出中的 Python 代码块、用 `ast` 校验语法、失败后把错误回灌给模型重试。 ### 4.1 定义模型客户端接口 为了便于测试,我们先定义一个统一接口,并提供一个模拟客户端。这个客户端第一次调用时故意返回语法错误的代码,第二次返回正确的代码,这样可以直观看到重试闭环的效果。 ```python # harness_mini.py from typing import Protocol class LLMClient(Protocol): def generate(self, prompt: str) -> str: """根据提示词生成文本。""" ... class FakeLLMClient: def __init__(self): self.count = 0 def generate(self, prompt: str) -> str: self.count += 1 if self.count == 1: return "```python\ndef add(a, b)\n return a + b\n```" return "```python\ndef add(a, b):\n return a + b\n```"

这里把模型客户端抽象成Protocol,是为了将来接入真实模型时不需要改动 Harness 主逻辑。FakeLLMClient演示了"首次失败、重试成功"的过程。

4.2 实现代码块解析与语法校验

接下来写两个工具函数。第一个从 Markdown 文本中提取 Python 代码块;第二个用ast.parse做语法校验。

# harness_mini.py import ast import re def extract_code_from_markdown(text: str) -> str: """解析模型输出中的 python 代码块,如果没有代码块则返回去除首尾空白的原始文本。""" pattern = re.compile(r"```(?:python)?\s*\n(.*?)```", re.S) match = pattern.search(text) if match: return match.group(1) return text.strip() def validate_python_code(code: str) -> tuple[bool, str]: """使用 ast 模块做语法校验,返回 (是否通过, 错误信息)。""" try: ast.parse(code) return True, "" except SyntaxError as e: line_no = e.lineno if e.lineno is not None else "未知" return False, f"第 {line_no} 行: {e.msg}"

extract_code_from_markdown中用到的正则r"```(?:python)?\s*\n(.*?)```"匹配以 ``` 包裹的代码块,并支持python语言标记。re.S表示让.能匹配换行符。

validate_python_code把代码交给ast.parse。如果语法错误,Python 会抛出SyntaxError,我们从中提取行号和错误信息,这比直接把完整异常文本丢给模型更友好。

4.3 编写重试主循环

核心 Harness 类就写在这里。它负责拼装提示词、调用模型、解析代码、校验语法,并在失败时把错误信息反馈给模型。

# harness_mini.py class CodeGenHarness: def __init__(self, client: LLMClient, max_retries: int = 3): self.client = client self.max_retries = max_retries def run(self, user_prompt: str) -> str: system_prompt = ( "你是一个严谨的 Python 代码生成助手。\n" "必须输出如下格式:\n" "```python\n" "<完整可运行的 Python 代码>\n" "```\n" "不允许输出任何多余解释。" ) full_prompt = f"{system_prompt}\n\n用户需求:{user_prompt}" for attempt in range(1, self.max_retries + 1): raw = self.client.generate(full_prompt) code = extract_code_from_markdown(raw) ok, error = validate_python_code(code) if ok: return code # 把错误信息回灌给模型,要求其修复 fix_prompt = ( f"你上一次生成的代码存在语法错误:\n{error}\n" f"错误代码:\n```python\n{code}\n```\n" f"请重新生成修复后的完整代码。" ) full_prompt = fix_prompt raise RuntimeError(f"经过 {self.max_retries} 次尝试仍未生成合法 Python 代码")

这段代码的核心逻辑集中在run方法中。每一次尝试都完成"生成 → 解析 → 校验"的循环,失败后构造新的修复提示词。注意,修复提示词里同时携带了错误信息和错误代码,这比只告诉模型"你错了"有效得多。

4.4 运行与结果说明

在__main__中跑一次完整流程:

# harness_mini.py if __name__ == "__main__": client = FakeLLMClient() harness = CodeGenHarness(client, max_retries=3) result = harness.run("写一个两数相加的函数") print("最终生成代码:") print(result) print(f"模型调用次数:{client.count}")

预期输出:

最终生成代码: def add(a, b): return a + b 模型调用次数:2

第一次生成的代码缺少冒号,导致SyntaxError,Harness 把错误反馈给模型;第二次生成正确代码并返回。这就是一个最基础的 Harness 闭环。

5. 实战二:结构化输出 Harness

代码生成之外,另一个常见场景是让模型输出 JSON 配置、接口定义、测试用例等结构化内容。这时候,Harness 的校验重点从语法正确性变成 Schema 符合性。

5.1 为什么用 JSON Schema

JSON Schema 是一种用于描述 JSON 数据结构的规范。它定义了一个 JSON 对象应该包含哪些字段、字段类型是什么、哪些字段必填、枚举值有哪些等。

在可控代码生成中,JSON Schema 的价值在于让模型的输出从"看起来像 JSON"变成"严格符合业务预期"。比如我们需要模型返回一个"分页查询结果",直接返回 JSON 是不够的;必须保证page是正整数、status是枚举值、name是非空字符串。这些约束都可以写进 Schema。

5.2 实现 JSON 输出校验闭环

下面的代码实现了一个通用 JSON Schema Harness:

# harness_json.py import json from typing import Any try: from jsonschema import validate, ValidationError except ImportError: raise ImportError("缺少依赖:请先执行 pip install jsonschema") class JsonHarness: def __init__(self, client, schema: dict[str, Any], max_retries: int = 3): self.client = client self.schema = schema self.max_retries = max_retries def _parse_json(self, raw: str) -> Any: try: return json.loads(raw) except json.JSONDecodeError as e: return e def run(self, prompt: str) -> Any: system_prompt = ( "你是一个结构化输出助手。\n" "必须只输出一个合法的 JSON 对象,不允许包含 Markdown 代码块,\n" f"JSON 必须满足以下 Schema:\n{json.dumps(self.schema, ensure_ascii=False, indent=2)}\n" ) full_prompt = f"{system_prompt}\n用户需求:{prompt}" for attempt in range(1, self.max_retries + 1): raw = self.client.generate(full_prompt) parsed = self._parse_json(raw) if isinstance(parsed, json.JSONDecodeError): fix_prompt = ( f"你上一次输出不是合法 JSON:{parsed.msg}。\n" "请重新输出一个合法的 JSON 对象。" ) full_prompt = fix_prompt continue try: validate(instance=parsed, schema=self.schema) return parsed except ValidationError as e: path = "/".join(str(p) for p in e.path) or "$" fix_prompt = ( f"你上一次输出的 JSON 不符合 Schema:{e.message}\n" f"错误路径:{path}\n" "请重新输出符合 Schema 的 JSON 对象。" ) full_prompt = fix_prompt raise RuntimeError(f"经过 {self.max_retries} 次尝试仍未获得符合 Schema 的 JSON")

与第一个示例相比,这个 Harness 新增了两种失败分支:JSON 解析失败和 Schema 校验失败。两种分支都需要把错误信息转化为下一次生成的提示词,形成闭环。

5.3 运行与结果说明

下面用一个演示客户端模拟模型输出 JSON:

# harness_json.py if __name__ == "__main__": class DemoClient: def generate(self, prompt: str) -> str: # 生产环境中替换为真实模型调用 return '{"name": "用户模块", "status": "active", "page": 1}' schema = { "type": "object", "properties": { "name": {"type": "string"}, "status": {"type": "string", "enum": ["active", "inactive"]}, "page": {"type": "integer", "minimum": 1} }, "required": ["name", "status", "page"] } harness = JsonHarness(DemoClient(), schema) result = harness.run("解析一段配置") print(result)

运行后输出:

{'name': '用户模块', 'status': 'active', 'page': 1}

如果模型返回的 JSON 缺失page字段,Harness 会捕获ValidationError,并把"错误路径:$"和错误信息传回给模型,要求其补齐字段。这个机制在批量数据处理、接口参数生成、配置项生成等场景中非常实用。

6. 实战三:把 Harness 扩展到可执行代码沙箱

语法校验通过,不代表代码真的能运行。对于自动化代码生成平台来说,更进一步的 Harness 是"执行验证"——在受控环境中实际运行模型生成的代码,用真实运行结果作为反馈。

6.1 从语法校验到运行校验

语法校验只能保证代码符合语言规则,无法发现运行时错误,比如变量未定义、函数调用参数错误、类型不匹配等。要想让模型生成的代码具备真正的可执行性,必须增加运行时校验环节。

这一步的关键是"受控"。模型生成的代码不可信,不能在宿主环境直接运行,否则可能出现恶意操作或意外资源消耗。

6.2 在临时目录中执行模型代码

下面这个示例使用临时目录加子进程的方式执行模型生成的代码,并通过超时限制防止死循环:

# harness_execute.py import subprocess import tempfile from pathlib import Path class ExecuteHarness: def __init__(self, client, max_retries: int = 3, timeout: int = 10): self.client = client self.max_retries = max_retries self.timeout = timeout def run(self, requirement: str) -> str: system_prompt = ( "你是一名可以生成可执行 Python 脚本的助手。\n" "只输出 Python 代码,使用如下格式包裹:\n" "```python\n<代码>\n```\n" "代码中不要包含任何交互输入,不要包含无限循环。" ) prompt = f"{system_prompt}\n需求:{requirement}" for attempt in range(1, self.max_retries + 1): raw = self.client.generate(prompt) code = extract_code_from_markdown(raw) ok, error = validate_python_code(code) if not ok: prompt = f"语法错误:{error}\n请重新生成完整代码。" continue with tempfile.TemporaryDirectory() as tmp: script = Path(tmp) / "gen_code.py" script.write_text(code, encoding="utf-8") try: proc = subprocess.run( ["python", str(script)], capture_output=True, text=True, timeout=self.timeout, ) except subprocess.TimeoutExpired: prompt = ( "代码执行超时,请检查是否存在死循环或长时间阻塞。\n" "请重新生成更高效的代码。" ) continue if proc.returncode == 0: return code, proc.stdout prompt = ( f"代码运行报错:\n{proc.stderr[:500]}\n" "请分析错误并重新生成修复后的完整代码。" ) raise RuntimeError(f"经过 {self.max_retries} 次尝试,代码仍无法通过执行校验")

这个类的写法和前面的 Harness 保持一致,但在校验阶段调用了subprocess.run。如果模型代码能正常运行并返回状态码 0,就认为代码通过校验。

6.3 安全边界提示

这里必须再次强调:直接执行模型生成的代码存在严重安全风险。模型可能生成删除文件、读写系统目录、下载远程脚本等危险操作。即使没有任何恶意意图,代码也可能因为资源消耗导致机器卡顿。

生产环境中,至少应该做到下面两点:

  • 使用容器运行生成的代码,比如 Docker,配合内存、CPU、网络访问限制。
  • 在独立的低权限用户下运行,禁止访问业务系统内部网络和服务。

如果你所在团队还没有完善的安全隔离条件,建议先不要在宿主环境启用执行验证,只保留语法校验。

7. 常见问题与排查思路

在落地 Harness 时,团队最常遇到的问题有下面几类。我把问题现象、常见原因和解决思路整理成表格,方便排查时对照。

问题现象常见原因解决思路
多次重试仍返回非法代码提示词没有强制约束输出格式,模型持续输出解释文本在提示词中明确写出"只输出代码块,不允许额外解释",并在解析前清理多余文本
重试循环一直不结束错误信息没有正确回灌给模型,或者回灌的信息不完整检查闭环中的修复提示词是否包含上一次错误信息和错误代码片段
JSON 解析总失败模型返回的内容包含 Markdown 代码块、注释或前后缀先提取 JSON 区域再进行json.loads,而不是直接解析完整输出
语法通过但运行时崩溃缺少依赖、运行环境不一致或代码存在隐藏逻辑错误在沙箱中固定依赖版本,增加运行时校验并回传 stderr 给模型
子进程执行模型代码把机器卡死模型生成了死循环或资源消耗型代码设置timeout,限制 CPU 和内存,优先使用容器隔离
输出结果不稳定,同一需求多次生成结果差异大缺少系统提示词,采样参数温度过高对工程化生成任务使用较低温度,并在提示词中增加格式和风格约束

遇到问题时,我的排查顺序通常是:先看模型原始输出,确认是不是解析环节出了问题;再看提示词是否足够明确;最后才怀疑模型能力。很多时候,问题根源并不在模型本身,而是外层工程没有把约束做扎实。

8. 最佳实践与工程建议

8.1 提示词与格式约束设计

提示词是 Harness 的第一道关卡。我建议把系统提示词拆成三个部分:角色定义、输出格式约束、需求内容。角色定义让模型知道它是什么身份;输出格式约束告诉它怎么组织答案;需求内容才是每次动态变化的部分。

在有条件的情况下,尽量使用 JSON Schema 描述输出结构。相比自然语言描述,Schema 更精确,也更容易被程序化校验。模型输出经过校验之后再进入业务逻辑,能显著减少运行时错误。

8.2 安全与权限治理

Harness 让代码生成更可控,但不是绝对安全。凡是涉及模型生成内容执行、部署、数据访问的场景,都要额外关注安全边界。

  • 执行权限最小化:使用独立低权限账号运行生成代码。
  • 网络访问隔离:默认禁止生成代码访问外网,按需放行白名单域名。
  • 敏感信息保护:提示词和模型输出都可能包含敏感信息,日志中需要脱敏处理。
  • 变更审批:涉及数据库操作、生产环境配置的生成代码,必须走人工审批流程。

8.3 可观测性与回归

Harness 的价值在于"每一次生成行为都有迹可循"。建议把每次调用的输入提示词、模型原始输出、校验结果、重试次数、最终输出都记录下来。这些数据既用于问题回溯,也用于评估模型生成质量。

回归测试同样重要。你可以准备一组标准需求,定期运行 Harness,记录通过率和失败原因。任何一次提示词调整或模型版本升级,都应该跑一遍回归用例,确保生成质量没有回退。

8.4 从 Harness 走向 Agent 的演进路线

对大多数团队来说,直接做 Agent 风险很高。更务实的路线是:先把 Harness 做到成熟,再在可靠闭环上引入 Agent 的自主能力。

举例来说,第一版系统可以是"固定 Harness + 固定提示词",让模型生成代码并校验;第二版可以加入"多轮修复闭环";第三版才让 Agent 在多个工具之间自主选择。每一步都以可观测、可回滚为前提,不要一步跨到全自主。

9. 总结与学习路线

本文从"为什么代码生成难以可控"这个问题出发,介绍了 Harness 的核心概念、与 Agent 的边界,并通过三个实战示例演示了不同层级的可控代码实践。

如果你正在建设代码生成平台,我的建议是不要急着追求 Agent 化,而是先把 Harness 的"约束 → 校验 → 反馈"闭环做扎实。从 5 到 10 个高频代码生成场景入手,为每个场景定义输出 Schema、校验规则和重试模板,逐步沉淀出属于你自己团队的 Harness 工具包。这条路看起来慢,但长期收益最高,也最容易让业务方建立信任。

下一步可以继续深入的方向包括:如何针对特定框架编写语义校验规则、如何在 Harness 中接入单元测试结果、如何设计多模型路由与降级策略,以及如何让 Harness 在持续集成流水线中发挥作用。欢迎在评论区分享你团队在可控代码生成上的经验和问题,一起探讨更好的工程实践。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询