把同一个 Prompt 从 ChatGPT 里搬到一套带工具调用的智能体系统,结果完全不是一回事,这是我最近三个月最大的体感。前两天我还在为一个代码助手调整提示词,模型输出稍微偏一点,整个流程就崩;后来我把注意力从“Prompt 怎么写”挪到“模型怎么被装配进一个可控的框架里”,也就是 Harness 工程,问题才真正开始被解决。这篇内容想聊的就是这个转变:从 Prompt 到 Harness,AI 工程到底在演进什么,以及我从零搭一个轻量 Harness 的实操记录。适合正在做智能体、做 RAG、做 AI 应用落地的人,尤其是那种已经发现“提示词写得再好,系统还是不稳”的开发者。
1. 从Prompt到Harness:一次必然的范式转移
1.1 Prompt工程为什么重要,又为什么不够用
Prompt Engineering 的价值不用多讲:它是最低成本的模型交互方式。写好一段指令、给几个 few-shot 例子、设计好思维链,就能让模型输出接近预期。我在早期做 AI 工具时,几乎所有“业务逻辑”都靠 Prompt 硬撑,比如让模型输出固定 JSON、让模型判断用户意图、让模型从长文档里抽取字段。短时间内没问题,因为模型能力确实强,一段措辞激进的 Prompt 就能把行为掰过来。
但 Prompt 有它天然的边界:它只是一次模型调用的输入文本,管不了状态、管不了工具、管不了生命周期。一旦场景变成多轮对话、工具调用、错误恢复,问题就来了。模型返回了格式不对的 JSON,你要么重试,要么让用户重新说一遍;模型调完一个工具后忘了上一步的结果,你只能在上下文里反复强调。这些事靠 Prompt 解决,就像靠一封写得再详细的邮件去管理一个实习生——邮件可以告诉他今天该干什么,但没法保证他有权限、有资料、知道流程,更没法在他做错的时候自动纠正。
用一个生活一点的类比:Prompt 是给新员工写的一段“工作指令”,Harness 则是把岗位职责、审批流程、权限清单、工具链、复盘机制全部固定下来的管理体系。模型还是那个模型,但它工作的环境变了,行为稳定性就完全不一样。这也是为什么现在越来越多的团队把注意力从“调 Prompt”转向“搭 Harness”。
1.2 Harness到底在套什么
Harness 这个词直译是“马具”,在 AI 工程里更准确的理解是“一套包裹着模型的受控运行框架”。它不是把模型关进笼子,而是给模型装一个驾驶舱:什么信息可以进入模型,模型调用了什么工具,输出怎么被校验,意外情况怎么恢复,全部由 Harness 接管。
我实际体验过 DeepSeek Harness 这类工具之后,对“进出口控制”这个说法感受特别深。它桌面版可以本地部署,连接本地模型后可以开启思考模式,整个交互不是“发一条 prompt 拿一段回复”,而是模型在一个受控循环里不断读取状态、调用技能、产生结果。Harness 在输入侧做的事情包括:系统指令注入、历史记忆加载、工具定义拼装、检索结果摘要;在输出侧做的事情包括:结构化校验、内容安全护栏、重试策略、下游动作分发。模型本身只负责“下一步输出什么”,而 Harness 负责“这一步允许模型看到什么、下一步要拿模型的输出去做什么”。
这种设计最直接的好处是,Prompt 不再是唯一的行为控制点。以前你想让模型“用中文回答、输出 JSON、不要编造”,全写进一段 Prompt;现在这些约束可以拆到 Harness 的不同层:语言约束放系统指令,格式约束放输出解析器,事实约束放到检索与工具结果里。任何一个环节出问题,都能单独修,而不是整段 Prompt 推倒重来。
1.3 Agent与Harness:一个管决策,一个管运行
很多人会把 Agent 和 Harness 混在一起说,尤其是看到“DeepSeek Harness 多个智能体编排”这类词时更迷糊。我的理解是:Agent 是决策主体,它负责感知环境、决定下一步行动、调用工具;Harness 是运行框架,它负责让“感知-决策-行动”这个循环在一个稳定、可观测、可恢复的环境里转起来。
| 维度 | Agent | Harness |
|---|---|---|
| 核心定位 | 智能决策循环 | 运行控制框架 |
| 主要机制 | 推理、规划、工具调用 | 状态管理、护栏、重试、编排 |
| 关注点 | 模型怎么想 | 系统怎么跑 |
| 典型问题 | 决策对不对 | 出错了能不能恢复 |
| 例子 | ReAct Agent、Supervisor Agent | LangGraph 编排层、DeepSeek Harness |
一张表格就能看明白:Agent 关心的是“下一步该做什么”,Harness 关心的是“这一套流程怎么被安全地执行完”。有了 Agent 不代表有了 Harness,很多人的 ReAct Agent 跑起来乱跳、死循环、上下文爆炸,就是因为只有决策循环、没有控制框架。反过来,一个成熟的 Harness 里可以跑单个 Agent,也可以编排多个 Agent,这也是工程化的意义所在。
2. Harness架构拆解与设计思路
2.1 一个可靠Harness的最小组成
一个能上线的 Harness,再精简也要包含五个模块:
- 调度器:控制主循环,决定“模型→工具→模型→结束”的流转,约束最大迭代次数和 token 预算。没有它,Agent 可能陷入死循环。
- 记忆存储:管理短期会话上下文和长期业务记忆,每个 Agent 各读各的,避免全部塞进一条消息里。没有它,多轮对话和多智能体场景全都撑不住。
- 工具注册中心:统一维护工具的名称、参数 schema、权限级别和调用方式。没有它,模型面对的是一堆不可控的外部函数,权限和安全无从谈起。
- 护栏:在模型输入前拦截敏感请求,在模型输出后做内容与格式校验。没有它,模型输出可以直接炸掉下游流程。
- 模型接口:负责连接实际模型服务,统一处理超时、重试、流式输出和思考模式字段。没有它,上层业务会跟具体模型 SDK 耦合死。
这五个模块不是各干各的,它们的协作顺序决定了 Harness 的稳定程度。我通常把流程定成:调度器收到用户请求后,先从记忆存储装载上下文,再让护栏做输入检查,接着通过模型接口调用模型生成动作,如果是工具调用就走工具注册中心执行,结果写回记忆,然后回到调度器继续下一轮。这样一个闭环里,每个环节都可以插桩、打日志、加超时,问题定位非常清晰。
2.2 用LangGraph搭建可编排的Harness骨架
选 LangGraph 做 Harness 骨架,原因是它把流程表达成图状态机:节点是处理函数,边是流转条件,状态对象在节点间传递。这比手写 while 循环管理 Agent 状态要清晰得多,尤其是要加条件分支、中断恢复、多人协作介入点的时候。
一个最简骨架只需要一个状态类、三个节点函数和一条条件边:
from typing import Annotated, TypedDict from langgraph.graph import StateGraph, END class HarnessState(TypedDict): messages: list tools: list iterations: int def agent_node(state: HarnessState): # 调用模型,决定是回复用户还是调用工具 response = call_model(state["messages"]) return {"messages": state["messages"] + [response]} def tools_node(state: HarnessState): # 执行模型要求的工具调用 result = execute_tool(state["messages"][-1].tool_calls) return {"messages": state["messages"] + [tool_message(result)]} def should_continue(state: HarnessState): last = state["messages"][-1] if last.tool_calls and state["iterations"] < 10: return "tools" return END graph = StateGraph(HarnessState) graph.add_node("agent", agent_node) graph.add_node("tools", tools_node) graph.add_edge("agent", "tools", condition=should_continue) graph.set_entry_point("agent")这里的关键点是 iterations 字段:它不只是计数器,更是“安全阀”。很多 Agent 失控的原因不是模型不聪明,而是循环没有收敛条件,模型反复认为自己需要调用工具。我在实际项目里会把最大迭代次数设置成 8 到 12,超过后强制要求模型直接生成最终回复,必要时再叠加 token 预算检查。
另一个值得注意的功能是 checkpointer,也就是检查点机制。它可以把每一轮执行的状态持久化,系统中途宕机或网络闪断后,能从最近一个稳定的检查点恢复,而不是整轮重跑。对生产环境来说,这个能力比模型选型还重要。
2.3 工具注册与模型调用的两条路径取舍
让模型使用工具有两条主流路径:Function Calling 和提示词注入。
Function Calling 指的是模型原生支持结构化工具调用,模型会直接输出一个“调某个函数、传这些参数”的动作对象。这条路解析稳定、不容易出现格式错误,适合支持 function calling 的模型,尤其是需要严格参数校验的业务场景。提示词注入则是把工具说明写进 system prompt,让模型自己用文本形式输出调用意图(通常约定 JSON),适合老模型、不支持 function calling 的本地部署模型,或者你不想过分依赖供应商特有接口的场景。
| 对比维度 | Function Calling | 提示词注入 |
|---|---|---|
| 解析稳定性 | 高,模型原生输出结构化动作 | 低,需要自己解析文本 |
| 依赖程度 | 依赖模型供应商能力 | 任何文本模型都可用 |
| 上下文开销 | 工具 schema 仍需占用 | 工具说明和示例更占 token |
| 灵活度 | 受工具 schema 限制 | 可以在 prompt 里自由变通 |
| 推荐场景 | 生产系统、强约束工具链 | 快速验证、自定义模型接入 |
两条路在 Harness 里并不冲突。我现在的做法是:工具注册中心维护一份统一的工具 schema,优先走 Function Calling;当某个接入的模型不支持时,Harness 自动降级为提示词注入模式,把同样的 schema 渲染成文本塞给模型。这样底层的工具定义只有一份,切换模型不影响业务逻辑。
DeepSeek Harness 里常被提到的 skill,本质上也是把这条路再往上提一层:一个 skill 就是“一段固定的 prompt + 一组工具定义 + 一段后处理逻辑”的封装。模型不再面对散装工具,而是面对粒度更合适的“技能”,比如“分析项目结构”“生成接口文档”。这对降低决策难度、减少无效工具调用非常有帮助,我后面实操里也沿用了这个思路。
3. 从零实现一个轻量Harness:实操记录
3.1 环境准备与项目结构
我习惯先用最轻的方式搭一套可运行的 Harness,验证完再上 LangGraph 这类编排框架。这里的实操记录以 Python 为例,依赖尽量少:
pip install langgraph langchain-core openai如果接本地模型,需要准备一个兼容 OpenAI 协议的推理服务,比如 vLLM 或 Ollama 这类工具,端口通常留在 8000 或 11434 附近。项目结构我建议这样放:
harness_demo/ ├── main.py # 入口,启动交互 ├── harness.py # Harness 核心逻辑 ├── tools.py # 工具注册与执行 ├── memory.py # 会话记忆读写 ├── config.py # 模型地址、密钥、参数 └── skills/ ├── project_analyzer.py # 高频 skill 示例 └── doc_writer.py目录拆得干净一点,后面替换模型、增加工具、沉淀 skill 会顺手很多。很多项目一开始把所有代码塞进一个文件,到需要加第二个 Agent 的时候就开始痛苦,这个结构虽然简单,但模块边界已经够用。
3.2 核心模块实现:用一次可收敛的循环
核心的 Harness 类可以精简到几十行,重点在于把“循环-护栏-重试”固定成模板:
import json import time class Harness: def __init__(self, model_client, tools, max_iterations=10, system_prompt="", timeout=30): self.model = model_client self.tools = tools self.max_iterations = max_iterations self.system_prompt = system_prompt self.timeout = timeout self.messages = [{"role": "system", "content": system_prompt}] def run(self, user_input): self.messages.append({"role": "user", "content": user_input}) for i in range(self.max_iterations): response = self.model.chat( messages=self.messages, timeout=self.timeout) content = response["content"] tool_calls = response.get("tool_calls") if not tool_calls: self.messages.append( {"role": "assistant", "content": content}) return self.safe_answer(content) # 执行工具调用 for call in tool_calls: result = self.tools.execute(call["name"], call["arguments"]) self.messages.append({ "role": "tool", "tool_call_id": call["id"], "content": json.dumps(result, ensure_ascii=False) }) self.messages.append( {"role": "assistant", "content": content, "tool_calls": tool_calls}) # 超过迭代上限后的兜底 fallback = "系统未能完成本次任务,请调整描述后重试。" return self.safe_answer(fallback) def safe_answer(self, content): # 输出侧校验 if len(content) > 20000: content = content[:20000] + "...(已截断)" return content这里面有几个容易踩的细节。第一,每次模型响应如果带 tool_calls,一定要把原样写回 messages,并且把每条工具执行结果用 tool_call_id 对应上,否则模型下一次看到的就是一份残缺历史,容易重复调用同一个工具。第二,超时和重试要在模型接口层做,而不是在最外层做;我见过很多项目只在 catch 里打日志,然后整个流程就断了,正确做法是对网络错误做一次或两次退避重试。第三,fallback 文案不要指望模型生成,固定字符串最稳,因为此时已经是异常分支了。
3.3 接入本地模型并配置“思考模式”
本地模型的接入我一直用这种配置方式,放在 config.py 里:
import os MODEL_BASE_URL = os.getenv("MODEL_BASE_URL", "http://localhost:8000/v1") MODEL_NAME = os.getenv("MODEL_NAME", "deepseek-r1") ENABLE_THINKING = os.getenv("ENABLE_THINKING", "true").lower() == "true" MAX_TOKENS = int(os.getenv("MAX_TOKENS", "8192")) TEMPERATURE = float(os.getenv("TEMPERATURE", "0.6"))这里最需要注意的是思考模式。开启思考模式后,模型响应里除了正常的 content 字段,还会多出一个 reasoning_content 或者 thinking 字段,里面是模型的推理过程。很多人在接入时直接把整个响应塞回 messages,结果用户看到一堆“思考过程”,甚至上下文被推理文本撑爆。
我踩过的坑是:思考内容应该单独保存到日志或内存的 observability 通道里,用来调试模型决策,但不应该进入用户可见的对话历史,也不应该在下一次模型调用时当普通消息传回去。有些模型如果历史里混入了大量推理文本,会明显变“懒”,后续回复质量下降。DeepSeek Harness 连接本地模型时的思考模式配置,本质也是在处理这一层:让推理过程可见、可控、可追踪,但不污染最终交付内容。
3.4 多智能体编排:Supervisor模式实操
当任务复杂度超过单个 Agent 的处理能力时,就需要多个 Agent 协作。最简单实用的编排模式是 Supervisor(主管)模式:一个主管 Agent 负责任务拆解和结果裁决,多个工作 Agent 各司其职。在 LangGraph 里,这本质上是“主管节点决定把消息路由给哪个工作节点”。
def supervisor_node(state): decision = call_model([ {"role": "system", "content": "你是主管,决定下一步交给哪个agent。"}, *state["messages"] ]) return {"next": parse_next(decision)} def route_after_supervisor(state): if state["next"] == "coder": return "coder_agent" if state["next"] == "reviewer": return "reviewer_agent" return END多个智能体编排时,我特别强调“记忆分区”。不要让所有 Agent 共享同一份 memory 对象,否则 B Agent 会读到 A Agent 的中间草稿,输出变得颠三倒四。我通常给每个 Agent 加一个命名空间前缀,比如 coder.memory、reviewer.memory,主管 Agent 拥有一个全局 memory 用来读各 Agent 的结论摘要。这样一来,串扰问题基本被消灭,问题定位也更容易。
4. 实践中的常见问题与排查实录
4.1 提示词被内容策略拦截的应急处理
开发过程中最迷幻的报错之一就是 invalid prompt 这类提示,它表示你的输入被服务端的内容安全策略拦下了。这个问题的触发原因可能只是一个措辞,不一定是你真写了什么敏感内容。比如让模型“扮演一个越狱角色”来测试逻辑、或者输入里包含大段包含特定关键词的网页文本,都可能触发检查。
我现在的处理流程是:先把任务描述改成合规的业务化表达,明确说明用途和分析目标;再把超长输入拆成多个段落分批处理,降低单次请求的“关键词密度”;最后加一个重试机制,在收到这这类报错时提示用户调整输入,而不是直接让整个流程灰屏。这里尤其提醒一句:正确做法是调整表达和流程,而不是想着怎么绕过内容策略,那既不稳妥也不可持续。把安全护栏当成 Harness 的固有环节来设计,后续交付到复杂环境才不会被反复打回。
4.2 输出格式不稳与JSON解析失败
模型输出不稳是家常便饭。你要的是合法 JSON,它给你 Markdown 代码块;要的是 enum 值,它给你一句解释。以前我习惯在 Prompt 里疯狂补约束,比如“必须严格输出 JSON,不要写任何其他内容”,效果有,但还是会偶发失败。
后来我把重心移到 Harness 的输出解析层:先尝试按代码块提取,再尝试直接 JSON 解析,最后用正则兜底提取最外层花括号内容。三种策略都失败才走重试。代价是解析代码多了,但稳定率从“偶尔崩”变成“可预期”。还有一个细节是启用模型的 JSON Mode 或者 structured output 参数,这比任何 prompt 约束都可靠,因为约束发生在模型解码阶段而不是文本生成后的碰运气。
4.3 多个智能体上下文串扰
多智能体跑起来后,最常见的诡异现象是“A 写代码,B 评审,B 评论里出现了 A 的草稿片段”。排查下来,原因几乎都是消息总线或 memory 对象没有做隔离。解决不难:按角色命名空间分区,读写时指定 namespace,主管只读各 Agent 的最终结果摘要。另外一个隐蔽问题是中间状态的 tool_call_id 冲突,两个 Agent 各自调用工具时,id 必须区分开,否则模型会把失败的工具结果算到另一个 Agent 头上。串联 Agent 的名称为每个 id 加上前缀,是我实践下来的最稳方案。
4.4 版本选择与回退:升级翻车复盘
项目版本迭代也会给 Harness 带来大坑。我遇到过升级到新版本后会话恢复行为异常、skill 加载不兼容的情况,最后老老实实回退到 v0.1.5-rc.2 这个稳定版本才恢复正常。那次事故给我的教训有三条:任何 Harness 相关依赖都要锁定版本并写进 lock 文件;升级前必须跑一遍已有回归集,至少把常用 skill 和工具调用流程完整过一遍;保留旧版本的配置文件与启动脚本副本,以备快速回退。
pip freeze > requirements.lock # 回退时直接安装锁文件里的版本 pip install -r requirements.lock我也见过桌面客户端类工具闪退的问题,通常和上下文超长、内存占用过高有关。排查时先看日志文件,如果日志里有 OOM 或 buffer 超限的记录,就该限制上下文窗口、缩短单轮工具结果长度,或者换用更轻量的模型配置。这类问题在 Harness 里特别值得处理,因为它不像普通 API 调用那样看得到明显错误,而是整个进程没了。
5. 从工程视角看Harness的下一步
5.1 Harness工程的核心价值:可测试、可观测、可回滚
把话题从具体工具拉回工程层面。模型能力像发动机,Harness 是底盘:发动机再强,没有转向、刹车和仪表盘,车就没法上路。Harness 工程最值钱的三件事,第一是可测试性——Prompt、skill、工具组合都应该是可以放进 CI 的测试用例,而不是靠人肉在对话框里点;第二是可观测性——每次模型调用的输入输出、token 消耗、推理过程、工具结果都要有记录,出了事故能回溯;第三是可回滚——所有行为变更都应该有版本,异常时能一键切回旧逻辑。
这三点在纯 Prompt 时代几乎做不到,因为行为藏在文本里,没法 diff、没法版本化、没法自动化回归。到了 Harness 阶段,行为被结构化成代码和配置,工程手段就全部可以套用了。这是我认为“从 Prompt 到 Harness”最本质的演进:AI 应用从“调参的艺术”变成了“可维护的系统”。
5.2 落地路径建议:从Prompt-only到平台化
不是每个项目都需要一上来就搭全套 Harness,但演进路径值得提前规划。我建议按这样的阶段走:
- 阶段一:Prompt 模板管理。把高频的 Prompt 统一存放、统一变量替换,至少先把重复劳动消掉。
- 阶段二:函数封装。把一个完整的“取上下文→调模型→校验输出→返回结果”流程包成一个函数,让业务方不直接碰模型 SDK。
- 阶段三:图编排。引入 LangGraph,把多步骤流程、条件分支、多 Agent 路由画成图,把循环和恢复机制落地。
- 阶段四:平台化。沉淀 skill、统一工具注册、加观测面板,让 team 里多个人可以协作维护。
一个很实用的切入点是:把你最常用的 Prompt 先沉淀成 skill。比如“分析项目结构好用的 prompt”,很多人天天在编辑器里手动复制一段指令去理解新仓库,但如果把它做成一个 skill:输入仓库路径,Harness 自动调用目录遍历、读关键文件、生成结构分析与依赖梳理,价值就不再是一个提示词,而是一个可复用工具。这算是我推荐的最小区块改造方案,从最简单的一两个高频场景开始,让团队先体会 Harness 的好处,再逐步铺开。
我在实际落地中还有一个习惯,会把每次都失败的 prompt 记录成“bad_case 样本”,把它们加进 Harness 的回归测试集里。只要一次修复让这类样本通过了,后面再升级模型或改动 skill 都不会轻易回归。这个习惯救了我好几次,比写任何架构文档都管用。
说实话,我自己刚接触 Harness 这个概念时也怀疑过:是不是换了个词包装老东西?真动手把一套带工具调用、带记忆隔离、带输出校验的框架搭起来之后,我才确认这确实是工程方式的转移。Prompt 仍然是重要的,它决定模型一层输出质量和风格;但如果你希望 AI 应用稳定地在生产环境里跑起来,决定下限的往往是 Harness。最后再分享一个小技巧:把模型的思考记录和最终交付内容分开存储,这个习惯可以让你在调试时同时拥有“模型的内心活动”和“用户看到的结果”,排查问题会快非常多。