
Harness 工程是围绕 AI 大模型智能体构建的一套支撑框架它解决的问题不是“模型有多聪明”而是“模型推理结果如何被安全、稳定、可调试地落地为真实操作”。在 Multi-Agent、Sandbox、Skill 这些概念越来越常见的今天理解 Harness 的设计思路比记住某个框架的接口更重要。接下来的内容会沿着一条完整的学习主线展开先搭建一个最小 Harness再逐步加入 Skill 注册、Sandbox 执行和 Multi-Agent 调度最后给出一套可复用的排查路径和生产化检查清单。这篇文章适合已经熟悉 Python 和基础模型 API 调用正准备从“调用模型聊天”进入“构建智能体应用”的开发者。1. 先理解 Harness 在 AI 大模型项目中解决什么问题1.1 Harness 是智能体的运行控制层很多初学者会把智能体等同于“模型 提示词”但实际工程里模型只是其中一个组件。一个能处理复杂任务的智能体至少需要完成以下工作接收用户输入维护多轮对话上下文调用模型生成决策解析模型是否要求调用工具执行工具把工具结果回传给模型再让模型继续决策直到给出最终答案。这些工作如果全部写在业务代码里项目很快就会失控。Harness 扮演的就是运行控制层它规定了模型、工具、用户输入之间的交互顺序负责消息组装、工具路由、结果回传、异常处理。你可以把 Harness 理解成一个“控制器”模型在控制器内部被反复调用工具也在控制器内部被统一调度。一个典型的 Harness 运行循环如下接收用户输入构造初始消息列表。把消息列表和工具描述发给模型。判断模型返回的是普通文本还是工具调用指令。如果是工具调用解析工具名和参数执行对应 Skill。把工具执行结果以 tool 消息形式追加到消息列表。继续调用模型直到模型不再请求工具。返回最终文本作为答案。这个过程就是智能体应用最常见的运行模式也是 Harness 工程最核心的内容。学 Harness本质上就是学好这个循环。1.2 Harness、Multi-Agent、Sandbox、Skill 的关系这四个概念经常被放在一起讨论但它们解决的问题并不相同Harness智能体运行时的控制框架负责调度、上下文和异常处理。Skill可以被模型调用的能力单元比如查询天气、执行计算、调用数据库。Sandbox工具代码和执行环境的隔离层避免不可信代码直接影响主进程。Multi-Agent多个不同职责的智能体协作Harness 负责在多智能体之间路由和通信。可以这样理解Harness 是总调度Skill 是能力插槽Sandbox 是能力的执行空间Multi-Agent 是多个 Harness 实例或 Agent 实例的组合。1.3 学习 Harness 工程需要掌握哪些前置知识学习 Harness 不需要很强的算法背景但需要具备几个基础能力Python 编程基础特别是函数、类、异常处理。会调用 OpenAI 兼容的模型接口理解 messages、tools、tool_calls 这些基本结构。了解一些进程隔离概念比如 subprocess、超时、输出捕获。有阅读日志和定位问题的耐心。如果原始项目使用的模型、框架或版本不同下面的代码都需要按实际情况调整。掌握原理后换一套接口成本很低。2. 搭建一个可以扩展的 Harness 最小环境2.1 环境准备与依赖说明建议使用 Python 3.10 或以上版本用虚拟环境管理依赖。最小实验只需要一个模型客户端库这里使用openai库因为它已经成为很多模型的兼容接口标准。mkdir harness-workshop cd harness-workshop python -m venv .venv source .venv/bin/activate pip install openai python-dotenv创建.env文件存放模型服务配置LLM_API_KEYyour_api_key LLM_BASE_URLhttps://api.openai.com/v1 LLM_MODELgpt-4o-mini这里要注意LLM_BASE_URL不一定只能填 OpenAI 官方地址。很多模型服务商都提供 OpenAI 兼容接口只需要把 base_url 改成对应服务地址即可。实际项目中要先确认接口兼容版本。2.2 项目目录结构为了后续扩展不混乱下面这个目录结构可以作为起点harness-workshop/ ├── .env ├── harness.py ├── llm_client.py ├── sandbox.py ├── skills/ │ └── calculator.py └── main.py这只是一个学习项目结构。生产环境通常还需要增加配置管理、日志目录、缓存、监控和测试目录。2.3 准备模型调用客户端llm_client.py的作用是统一封装模型请求让 Harness 不用关心底层是哪个模型服务。import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client OpenAI( api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL, https://api.openai.com/v1), ) def chat(messages, toolsNone, modelNone, max_tokens1024): model model or os.getenv(LLM_MODEL, gpt-4o-mini) params { model: model, messages: messages, max_tokens: max_tokens, } if tools: params[tools] tools params[tool_choice] auto return client.chat.completions.create(**params)关键点在于tools参数。模型支持工具调用时客户端会把工具描述发送给模型模型在需要时会返回tool_calls。如果不传tools模型就只能输出普通文本。2.4 验证最小链路先写一个最简单的调用脚本验证模型接口可以正常工作。from llm_client import chat response chat([{role: user, content: 你好请用一句话介绍自己}]) print(response.choices[0].message.content)运行后看到模型返回文本就说明环境正常。这一步的价值是先把网络、密钥、模型名这些容易出错的变量排除掉后续写 Harness 时才不会因为接口问题干扰判断。3. 实现 Harness 骨架消息循环、Skill 注册与 Sandbox 执行3.1 核心数据结构Harness 的核心数据结构就是消息列表。一个消息是一个 dict至少包含role和content。工具调用场景下还会出现tool_calls、tool_call_id等字段。OpenAI 兼容接口的常见消息角色包括system系统提示词。user用户输入。assistant模型输出。tool工具执行结果必须带tool_call_id对应该次工具调用。只要遵循这个结构模型就能正确理解上下文。3.2 消息循环主流程实现一个最简 Harness重点看 run 方法里的循环。import json class Harness: def __init__(self, modelNone, max_iterations5): self.model model self.max_iterations max_iterations self.skills {} self.sandbox None def register_skill(self, name, description, parameters, handler): self.skills[name] { description: description, parameters: parameters, handler: handler, } def get_tool_schema(self): schemas [] for name, skill in self.skills.items(): schemas.append({ type: function, function: { name: name, description: skill[description], parameters: skill[parameters], }, }) return schemas def execute_tool(self, tool_call): name tool_call.function.name arguments json.loads(tool_call.function.arguments or {}) skill self.skills.get(name) if not skill: return fSkill {name} not found try: return skill[handler](**arguments) except Exception as exc: return fError: {exc} def run(self, user_input, system_promptNone): messages [] if system_prompt: messages.append({role: system, content: system_prompt}) messages.append({role: user, content: user_input}) for _ in range(self.max_iterations): response chat(messages, toolsself.get_tool_schema(), modelself.model) message response.choices[0].message messages.append(message.model_dump(exclude_noneTrue)) if not message.tool_calls: return message.content for tool_call in message.tool_calls: result self.execute_tool(tool_call) messages.append({ role: tool, tool_call_id: tool_call.id, content: result, }) return 达到最大迭代次数未生成最终回答。这里有两点值得注意。第一max_iterations是防止模型反复调用工具而设计的没有这个限制个别场景下模型会一直请求工具造成资源浪费。第二每次工具结果都要追加到消息列表否则模型无法知道工具执行结果后续决策就没有依据。3.3 Skill 注册中心Skill 是 Harness 暴露给模型的能力。注册一个 Skill 需要四个信息名称、描述、参数 Schema、处理函数。def register_calculator(harness): harness.register_skill( namecalculate, description计算一个数学表达式的值例如 1 2 * 3, parameters{ type: object, properties: { expression: { type: string, description: 要计算的数学表达式, } }, required: [expression], }, handlercalculate, )参数 Schema 必须使用 JSON Schema 格式这是模型理解参数结构的接口协议。描述质量直接决定模型能不能正确调用。3.4 轻量 Sandbox 执行器Sandbox 负责隔离代码执行。先实现一个轻量版本用subprocess运行一段 Python 代码捕获输出设置超时。import subprocess import tempfile import os class Sandbox: def run_code(self, code: str, timeout: int 10): with tempfile.TemporaryDirectory() as tmpdir: script_path os.path.join(tmpdir, main.py) with open(script_path, w, encodingutf-8) as f: f.write(code) try: proc subprocess.run( [python, script_path], capture_outputTrue, textTrue, timeouttimeout, ) if proc.returncode ! 0: return fexit_code{proc.returncode}\nstderr{proc.stderr} return fstdout{proc.stdout.strip()} except subprocess.TimeoutExpired: return ftimeout after {timeout} seconds这个 Sandbox 的作用是把不可信代码从模型进程中隔离开。即使代码崩溃也不影响主进程。但它只是“进程隔离”不是完整安全方案生产环境还需要容器级隔离。3.5 跑通一个最简单的工具调用把 Harness、Skill、Sandbox 组合起来实现一个能“写代码并执行”的最小智能体。from harness import Harness from sandbox import Sandbox from skills.calculator import register_calculator harness Harness() harness.sandbox Sandbox() register_calculator(harness) def run_python_code(code: str) - str: return harness.sandbox.run_code(code) harness.register_skill( namerun_python_code, description运行一段 Python 代码返回标准输出, parameters{ type: object, properties: { code: { type: string, description: 要执行的 Python 代码, } }, required: [code], }, handlerrun_python_code, ) if __name__ __main__: result harness.run(请计算 12 * 7并运行代码验证一下) print(result)运行后模型可能会先调用calculate也可能直接调用run_python_code。无论走哪条路径Harness 都会把工具结果回传给模型再由模型组织最终回答。这个例子虽然小但已经完整覆盖了 Harness 的主循环。4. 从单 Agent 到 Multi-Agent 协作4.1 为什么单个 Agent 会被 Harness 调度复杂任务单个 Agent 在任务类型单一、工具较少时表现很好。但真实项目里任务往往横跨多个领域用户可能既要查数据库又要生成报表还要调用外部 API。如果所有工具都塞进一个 Agent工具列表会越变越长模型在选择工具时很容易混淆。更合理的方式是把职责拆开一个 Agent 负责编程一个 Agent 负责写作一个 Agent 负责检索。拆开后每个 Agent 的 Harness 只需要注册自己领域的 Skill工具列表短模型决策准确率更高。4.2 Multi-Agent 调度器的职责边界Multi-Agent 并不是简单地把多个 Harness 堆起来而是需要一个调度器决定“用户请求交给哪个 Agent”。调度器的职责包括解析用户意图。决定使用哪个 Agent。传递上下文。汇总多个 Agent 的结果。控制协作轮次防止无限循环。调度器本身可以用规则实现也可以用模型实现。学习阶段建议先写规则因为规则更容易排查。4.3 用任务路由实现多 Agent 分工下面是一个基于关键词路由的简单调度器。class Agent: def __init__(self, name, instruction, harness): self.name name self.instruction instruction self.harness harness class Orchestrator: def __init__(self, agents): self.agents agents def route(self, user_input): if 代码 in user_input or python in user_input.lower(): return self.agents[coder] if 文案 in user_input or 标题 in user_input: return self.agents[writer] return self.agents[general] def handle(self, user_input): agent self.route(user_input) return agent.harness.run(user_input, system_promptagent.instruction)这里每个 Agent 都有自己的 Harness 和 Skill。调度器只负责路由不直接处理业务。当任务需要多个 Agent 协作时可以增加一层“计划器”先让一个 Agent 拆解任务再把子任务分发给其他 Agent最终汇总结果。这一层需要额外控制轮次否则协作可能失控。4.4 如何避免 Multi-Agent 循环与上下文膨胀Multi-Agent 最常见的两个问题第一个是死循环。Agent A 给 Agent B 发消息Agent B 处理后又把结果发回给 Agent A两个 Agent 来回传递没有终止条件。解法是给整个调度器设置总轮次上限并在每轮传递时检查输出是否已经有明确结论。for round_idx in range(max_rounds): result agent.process(context) if result.is_final: break context.append(result.message)第二个是上下文膨胀。每个 Agent 都携带完整历史协作轮次多了以后上下文很快超过模型窗口。解法是只传递必要的摘要而不是每次都传递全部历史。实际项目中可以先从小规模单 Agent 起步确认 Harness 稳定后再拆 Multi-Agent。不要为了“多 Agent”而多 Agent。5. Skill 工程化的常见误区与设计规范5.1 Skill 的结构描述、参数、实现、校验一个可维护的 Skill 不应该只是一个函数。建议包含以下内容能力名称。能力描述。参数 Schema。参数校验逻辑。核心实现。异常返回约定。尤其要注意异常返回约定。Skill 执行失败时不要把堆栈直接返回给模型而是返回结构化错误信息比如{error: invalid_expression, message: 表达式不支持变量}。模型看到这类信息后可以尝试修正参数重新调用。5.2 写一个可被模型理解的 Skill 清单描述要具体。对比下面的写法差处理计算好计算一个数学表达式的值例如 1 2 * 3不支持变量和函数调用参数描述也很重要。比如expression字段要写清楚格式、单位、示例。parameters{ type: object, properties: { expression: { type: string, description: 数学表达式示例1 2 * 3, } }, required: [expression], }模型工具选择的准确性很大程度上依赖描述质量而不是模型本身强大与否。5.3 Skill 命名、描述与参数 Schema 的三个坑第一个坑命名模糊。比如handle_data这个名字就不如query_user_order清晰。命名要能直接表达“做什么”。第二个坑描述太短或太长。太短会让模型误解太长会占用上下文。理想长度是 1 到 3 句话突出输入、输出和边界。第三个坑参数 Schema 与真实实现不一致。模型按 Schema 传参如果 handler 期待字段名不一致就会出现 TypeError。推荐在 register 时写一个校验器先校验参数再调用 handler。5.4 Skill 版本与回滚Skill 会迭代同一个能力可能有 v1、v2 两个版本。建议在 Skill 名称中带版本例如query_user_order_v2或者通过 Harness 的 route 层控制版本切换。生产环境中要记录每个 Skill 的调用日志包括调用时间、参数、返回结果、耗时。这样一旦新版本出现问题可以快速回滚到旧版本。6. Sandbox 隔离从学习环境到生产环境6.1 为什么工具执行需要沙箱模型生成的代码可能会访问文件系统、网络、系统命令甚至包含恶意逻辑。如果直接在 Harness 所在进程中执行模型的一次错误输出就可能导致整个服务崩溃。Sandbox 的价值在于限制代码运行环境把风险控制在隔离空间内。学习环境的 Sandbox 可以用subprocess实现但它只能隔离崩溃和超时不能防范恶意行为。生产环境必须从进程级隔离升级到容器级隔离或云沙箱服务。6.2 subprocess 轻量沙箱实现前面已经给出了一个轻量实现。这里再补充一些关键参数subprocess.run( [python, -u, script_path], capture_outputTrue, textTrue, timeouttimeout, cwdtmpdir, env{}, )-u表示不缓冲输出便于及时捕获。cwdtmpdir把工作目录锁定在临时目录避免读取项目文件。env{}清空环境变量能减少一部分信息泄露风险。这些参数能提升安全性但依然不是完整沙箱。学习时要明白边界在哪里。6.3 沙箱的安全边界并不等于安全防护不要误以为“用了 subprocess 就安全了”。Python 子进程仍然可以访问网络、读写/tmp以外的路径甚至尝试系统调用。要真正隔离恶意代码需要更底层的手段比如容器隔离Docker/Kubernetes。轻量虚拟机microVM。云厂商的 Serverless 沙箱。生产环境中最好是“每个模型调用或每个工具执行独立沙箱”任务结束后销毁。这样可以避免不同任务之间的数据污染。6.4 生产环境 Sandbox 方案对比与升级路径方案隔离级别启动速度成本适用场景subprocess进程级快低学习、Demo、可信代码Docker 容器容器级中中常规生产工具执行Kubernetes Job容器级较慢较高批量任务、并行执行microVM虚拟机级较慢高高安全要求场景升级路径通常是先用 subprocess 跑通流程再迁移到 Docker 容器最后根据成本和安全要求决定是否使用更重方案。7. Harness 排错手册现象、根因、检查和修复7.1 模型迟迟不输出 tool_calls现象模型总是返回普通文本明明应该调用工具却自己“猜一个答案”。常见原因没有传tools参数。工具描述不清晰。系统提示词没有说明必须调用工具。模型能力和参数设置不支持工具调用。检查方式python -c from llm_client import chat r chat([{role: user, content: 请调用计算工具11}], toolsharness.get_tool_schema()) print(r.choices[0].message) 修复建议先确认请求中真的带了tools再检查工具的 description 是否明确最后在系统提示词中写清楚“需要计算时必须调用 calculate 工具”。7.2 Skill 注册成功但调用报错现象模型确实调用了 Skill但 Harness 返回Error: xxx。常见原因Skill 名和数据字典里的名称不一致。参数 Schema 字段与 handler 参数名不匹配。handler 内部抛异常但未处理。检查方式打印tool_call.function.name和arguments确认实际收到的参数。修复建议在execute_tool中增加异常捕获并把错误信息返回给模型让模型有机会修正参数后重试。7.3 Sandbox 命令超时或权限失败现象工具调用长时间不返回或返回permission denied。常见原因代码中存在死循环。timeout 设置过短。subprocess 使用了错误的工作目录或环境变量。检查方式先在本地手动运行同样代码观察是否超时再查看 Harness 日志中的 stderr 输出。修复建议设置合理 timeout把工作目录固定到临时目录不要在生产环境直接使用 subprocess 执行不可信代码。7.4 Multi-Agent 陷入死循环现象多个 Agent 之间消息来回传递一直不输出最终结果。常见原因调度器没有终止条件或 Agent 输出结果一直包含“需要进一步处理”的语义。检查方式在调度器每一轮打印round_idx、agent_name、result。修复建议增加max_rounds上限增加结果判定逻辑当输出文本已完整回答用户问题时立即终止必要时用独立的“评审 Agent”判断是否结束。7.5 日志与可观测性建议排错的前提是可观测性。Harness 里至少要记录以下信息每次模型请求的输入消息数和工具列表长度。每次工具调用的名称、参数、返回结果、耗时。每次循环的轮次。异常堆栈和错误码。建议使用结构化日志比如 JSON 格式便于后续采集和分析。{ event: tool_call, skill: calculate, arguments: {\expression\: \11\}, result: 2, duration_ms: 35, round: 2 }有了这些日志才能快速定位问题发生在模型层、工具层还是调度层。8. 从教程项目走向生产环境的十个检查点8.1 学习环境和生产环境的核心差异维度学习项目生产项目配置写死在代码或 .env配置中心或环境变量日志print结构化日志和监控沙箱subprocess容器或云沙箱模型单一模型多模型路由和降级工具少数 Skill大量 Skill需要版本管理多 Agent固定路由动态规划和任务编排安全基本异常处理权限、限流、审计回滚手动重启版本发布和自动回滚测试手工验证单元测试和回归测试成本不考虑需要控制 token 消耗8.2 上线前检查清单确认所有密钥都不在代码仓库里。确认每个 Skill 都有异常返回约定不会抛出未捕获异常。确认 Sandbox 限制了超时、工作目录和环境变量。确认 Harness 设置了最大迭代次数。确认 Multi-Agent 调度器设置了总轮次上限。确认日志记录了消息、工具调用和耗时。确认模型在关键场景下不会输出危险操作。确认工具权限最小化只开放必要能力。确认有版本回滚方案。确认 token 消耗有监控和告警。8.3 下一步的扩展方向跑通这篇文章的 Harness 骨架后可以继续从四个方向深入强化工具层接入真实数据库、HTTP API 和文件系统并补齐权限控制。强化沙箱把 subprocess 替换成 Docker 容器编写容器镜像和资源限制。强化调度用模型决策代替关键词路由增加任务规划和结果评审。强化可观测性接入链路追踪和指标采集把 Harness 接入现有监控体系。Harness 工程的核心不是某个框架而是把模型能力工程化的能力。动手把消息循环跑通再逐步加人 Skill、Sandbox 和 Multi-Agent 后你会发现自己已经掌握了一条可复用的智能体开发主线。建议先保持单 Agent 结构直到日志、排错和 Skill 管理都稳定了再拆多 Agent。这样每一步出问题时你都能准确知道该去哪个模块排查。