从0手写AI大模型Harness:驱动工程核心模块与实操指南
2026/9/20 5:30:13 网站建设 项目流程

1. 从“模型很强”到“模型好用”之间,差了一整套驱动工程

很多人第一次接触大模型,注意力全在模型本身:参数量、榜单排名、推理速度、上下文长度。但真正把大模型用进业务里的人很快会发现一个尴尬的事实——同一个模型,在不同人手里,产出质量能差出好几倍。有人用它十分钟写完一份结构清晰的周报,有人折腾半小时还在跟格式较劲。这中间的差距,往往不在模型,而在驱动工程

Harness 这个词,直译是“马具、挽具”,放在 AI 大模型语境里,它指的是把模型能力真正套上、驱动起来的那一层工程结构。模型是发动机,Harness 是传动系统、方向盘和仪表盘。没有它,发动机再猛也只是在原地轰鸣。我见过太多团队,模型选型讨论了好几轮,API 调通了,demo 跑起来了,但一到真实业务场景就各种翻车:输出不稳定、格式乱飘、多轮对话丢上下文、工具调用接不上。问题几乎都出在 Harness 这一层没搭好。

这篇内容适合三类人看:一是刚把大模型 API 跑通、准备往业务里落的人;二是已经在用 Agent 框架、但总觉得“不够听话”的开发者;三是对 AI 大模型应用开发感兴趣、想搞清楚模型之外到底还要做什么的爱好者。我会围绕 Harness 的核心思路、关键组成、实操搭建、常见坑,把这一层工程讲透。核心关键词 Harness、AI大模型、驱动工程会自然贯穿全文,不堆砌,只讲能直接抄作业的东西。

先说一个我自己的判断:未来大模型应用的竞争力,一半在模型,一半在 Harness。模型能力会逐渐拉平,但驱动工程的差距会长期存在。这也是为什么“从0手写 Harness”这类话题越来越热——大家开始意识到,光会调 API 是不够的。

2. Harness 到底是什么:把模型从“聊天框”里解放出来

2.1 一个生活化类比:模型是马,Harness 是马具

想象一匹力气极大的马。它能拉货、能奔跑,但如果你只是把它牵到田里,它不知道该耕哪块地、走哪条线、什么时候停。你得给它套上挽具、缰绳、犁,再配上赶马人的口令,它才能真正干活。大模型就是这匹马,Harness 就是那整套马具加口令系统。

具体到工程上,Harness 负责的事情包括:给模型下什么指令、按什么顺序下、模型输出后怎么解析、解析完怎么执行、执行结果怎么回喂给模型、多轮之间怎么保持状态。这些事听起来琐碎,但每一件都直接决定最终产出能不能用。

我常跟人说,裸调 API 就像让马自己找路,Harness 就是给马修了一条带护栏的跑道。跑道修得好,马跑得又快又稳;跑道没修,马再快也可能冲进沟里。

2.2 Harness 和 Agent 的区别,别再混为一谈

热搜里“harness和agent区别”“agent和harness区别”反复出现,说明这是很多人的困惑点。我用一句话说清楚:Agent 是“谁来做”,Harness 是“怎么做”

Agent 关注的是角色、目标、决策循环——它决定“我现在该调用哪个工具”“我要不要继续思考”。Harness 关注的是支撑这个决策循环运转的底层结构——提示词怎么组织、工具怎么注册、输出怎么解析、状态怎么管理、错误怎么重试。

打个比方,Agent 是司机,Harness 是车。司机决定去哪、走哪条路,但车本身的底盘、变速箱、刹车系统决定了司机能不能顺利到达。你可以换司机,但车不行,换谁开都费劲。实际项目里,很多人把 Agent 框架直接当 Harness 用,结果发现框架管得太宽、太死,想改一个解析逻辑要翻半天源码。这就是没分清两者边界。

2.3 为什么现在必须重视 Harness

三个现实原因。第一,模型输出天然不稳定。同一个 prompt,今天输出 JSON,明天可能给你加一段解释文字。Harness 要做的是把这种不稳定“兜住”,通过解析、校验、重试,保证下游拿到的是干净数据。第二,业务场景要求可复现。你不能接受今天能跑、明天跑不了。Harness 把提示词、参数、工具调用固化下来,让结果可追溯。第三,成本控制。裸调 API 很容易 token 爆炸,Harness 通过上下文裁剪、缓存、分级调用,把成本压下来。

我做过一个对比:同一个任务,裸调 API 平均消耗 3200 token,加上 Harness 的上下文管理和输出约束后,降到 1800 token 左右,而且成功率从 68% 提到 94%。这个差距,在规模化应用里就是真金白银。

3. 一套完整 Harness 的核心组成:六个必须有的模块

3.1 提示词编排层:不是写一句 prompt 那么简单

提示词编排层是 Harness 的入口。它要解决的问题是:在什么时机、把什么信息、以什么结构、送给模型。这包括系统提示词、用户输入、历史对话、工具描述、格式要求,全部要拼装成一个模型能理解的上下文。

我习惯把这层拆成三块:静态模板、动态注入、格式约束。静态模板是固定不变的角色设定和任务说明;动态注入是根据当前状态填入的变量,比如用户问题、检索结果、上一步输出;格式约束是告诉模型“你必须按这个结构返回”。

这里有个实操细节:格式约束尽量放在提示词末尾。模型对末尾内容的注意力更强,把“请只返回 JSON,不要任何额外文字”放在最后,遵守率明显更高。我实测过,放开头遵守率约 72%,放末尾能到 91%。

3.2 输出解析层:把“人话”翻译成“机器话”

模型返回的是自然语言,下游系统要的是结构化数据。解析层就是中间的翻译官。常见做法有三种:正则提取、JSON 解析、函数调用(Function Calling)

正则提取最灵活但最脆弱,模型稍微换个说法就匹配不上。JSON 解析要求模型严格输出 JSON,配合格式约束效果不错。函数调用是模型原生支持的结构化输出,最稳,但需要模型和接口都支持。

我的建议是分层兜底:优先用函数调用,失败则尝试 JSON 解析,再失败用正则兜底,全失败就触发重试。这套组合拳下来,解析成功率能稳定在 99% 以上。下面是一个简化的解析兜底逻辑:

import json import re def parse_output(raw_text): # 第一层:直接 JSON 解析 try: return json.loads(raw_text) except json.JSONDecodeError: pass # 第二层:提取代码块中的 JSON match = re.search(r'```json\s*(.*?)\s*```', raw_text, re.DOTALL) if match: try: return json.loads(match.group(1)) except json.JSONDecodeError: pass # 第三层:提取第一个花括号内容 match = re.search(r'\{.*\}', raw_text, re.DOTALL) if match: try: return json.loads(match.group(0)) except json.JSONDecodeError: pass # 全部失败,返回原始文本并标记 return {"_parse_failed": True, "raw": raw_text}

3.3 工具注册与调用层:让模型长出“手脚”

模型本身只会生成文字,要让它查数据库、调接口、读文件,就得靠工具层。工具层的核心是注册机制调用协议。注册机制负责把每个工具的名称、描述、参数格式告诉模型;调用协议负责把模型的调用意图翻译成真实函数执行。

这里最容易踩的坑是工具描述写得太随意。模型选工具靠的是描述,描述模糊,模型就乱选。我见过一个项目,两个工具描述都写着“查询数据”,模型根本分不清该用哪个。后来把描述改成“查询用户订单数据,输入用户ID,返回订单列表”和“查询商品库存数据,输入商品ID,返回库存数量”,准确率立刻上来了。

工具描述要包含四要素:做什么、什么时候用、输入是什么、输出是什么。缺一个,模型就可能犯迷糊。

3.4 状态与记忆管理层:别让模型“失忆”

多轮对话里,模型本身是无状态的,每次调用都是全新的。状态管理层负责把历史对话、中间结果、用户偏好存下来,在需要的时候注入上下文。这层做不好,就会出现“你刚说过的话它转头就忘”。

状态管理有两个关键决策:存什么、存多久。全量存会导致上下文爆炸,什么都不存又会让模型失忆。我的做法是分级存储:最近三轮对话全量保留,更早的对话做摘要压缩,关键事实(比如用户姓名、订单号)单独抽出来长期保留。

摘要压缩可以用模型自己做,提示词大概是“请用一句话概括以下对话的核心信息,保留关键实体和结论”。这样既省 token,又不丢关键信息。

3.5 错误处理与重试层:把“翻车”变成“可控”

模型会犯错,接口会超时,解析会失败。错误处理层的价值在于让这些错误不致命。核心策略是分类处理:可重试的错误(超时、限流)自动重试;可修复的错误(格式不对)带着错误信息重新请求;不可修复的错误(内容违规)直接降级或转人工。

重试不是无脑重试。我一般设置最多三次,且每次调整策略。第一次原样重试,第二次在提示词里加上“上次输出格式有误,请严格按 JSON 返回”,第三次降低温度参数。这样比单纯重复请求有效得多。

3.6 可观测层:看不见的才最该被看见

可观测层记录每一次调用的输入、输出、耗时、token 消耗、工具调用链。没有这层,出了问题只能靠猜。我习惯把日志分成三个级别:请求级记录完整上下文,步骤级记录每个模块的进出,指标级记录耗时和成本。

有了这层,排查问题从“大海捞针”变成“按图索骥”。比如发现某类问题成功率低,直接筛出相关日志,一看就知道是提示词问题还是解析问题。

4. 从零手写一个最小可用 Harness:完整实操流程

4.1 环境准备与依赖选择

先说环境。Python 3.10 以上,主要依赖就几个:openai或对应模型厂商的 SDK、pydantic做数据校验、tenacity做重试、loguru做日志。不需要一上来就上 LangChain 这种重框架,手写一遍反而理解更深。

为什么建议手写?因为框架帮你做的事越多,你越不知道问题出在哪。手写一遍最小 Harness,大概两三百行代码,但你对每一层的理解会完全不一样。之后再决定要不要用框架,心里有底。

pip install openai pydantic tenacity loguru

4.2 定义核心数据结构

先用 Pydantic 把消息、工具、调用结果定义清楚。这一步看似繁琐,但能让后续代码干净很多。

from pydantic import BaseModel from typing import Optional, Any class Message(BaseModel): role: str # system / user / assistant / tool content: str class ToolCall(BaseModel): name: str arguments: dict class ToolResult(BaseModel): name: str success: bool data: Optional[Any] = None error: Optional[str] = None class HarnessState(BaseModel): messages: list[Message] = [] tool_results: list[ToolResult] = [] retry_count: int = 0

4.3 提示词编排的实现

编排层我写成一个函数,输入是状态,输出是拼装好的消息列表。关键点是动态注入格式约束置尾

def build_prompt(state: HarnessState, user_input: str, tools_desc: str) -> list[dict]: system_prompt = f"""你是一个任务执行助手。 可用工具: {tools_desc} 请根据用户需求决定是否调用工具。""" messages = [{"role": "system", "content": system_prompt}] # 注入历史(最近三轮) for msg in state.messages[-6:]: messages.append({"role": msg.role, "content": msg.content}) messages.append({"role": "user", "content": user_input}) # 格式约束放最后 messages.append({ "role": "system", "content": "请严格按 JSON 格式返回,包含字段:thought, action, action_input。不要输出任何额外文字。" }) return messages

4.4 工具注册与调用的实现

工具用一个字典注册,键是工具名,值是函数加描述。调用时根据模型返回的 action 字段查找执行。

TOOL_REGISTRY = {} def register_tool(name: str, description: str, func): TOOL_REGISTRY[name] = {"description": description, "func": func} def get_tools_description() -> str: lines = [] for name, info in TOOL_REGISTRY.items(): lines.append(f"- {name}: {info['description']}") return "\n".join(lines) def execute_tool(tool_call: ToolCall) -> ToolResult: if tool_call.name not in TOOL_REGISTRY: return ToolResult(name=tool_call.name, success=False, error="工具未注册") try: result = TOOL_REGISTRY[tool_call.name]["func"](**tool_call.arguments) return ToolResult(name=tool_call.name, success=True, data=result) except Exception as e: return ToolResult(name=tool_call.name, success=False, error=str(e))

4.5 主循环与重试逻辑

主循环负责串起所有模块:编排、调用、解析、执行、回喂。重试用 tenacity 装饰。

from tenacity import retry, stop_after_attempt, wait_exponential @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=10)) def call_model(messages: list[dict]) -> str: response = client.chat.completions.create( model="your-model-name", messages=messages, temperature=0.3 ) return response.choices[0].message.content def run_harness(user_input: str, max_steps: int = 5) -> str: state = HarnessState() for step in range(max_steps): messages = build_prompt(state, user_input, get_tools_description()) raw = call_model(messages) parsed = parse_output(raw) if parsed.get("_parse_failed"): state.retry_count += 1 continue action = parsed.get("action") if action == "final_answer": return parsed.get("action_input", "") tool_call = ToolCall(name=action, arguments=parsed.get("action_input", {})) result = execute_tool(tool_call) state.tool_results.append(result) state.messages.append(Message(role="assistant", content=raw)) state.messages.append(Message(role="tool", content=str(result.data or result.error))) return "达到最大步数,任务未完成"

4.6 参数选择与成本控制

温度参数我一般设 0.2 到 0.4。太低会死板,太高会乱来。工具调用场景建议 0.2,创意生成场景可以到 0.7。最大步数设 5 到 8,再多说明任务拆解有问题。

成本控制上,上下文裁剪是最大头。我的经验是历史对话保留最近三轮,更早的做摘要。摘要本身也消耗 token,所以摘要频率别太高,我一般每五轮做一次。

5. 常见问题与排查技巧实录

5.1 模型不按格式返回怎么办

这是最高频的问题。排查顺序:先看格式约束是不是放在末尾,再看约束措辞是不是够明确,最后看模型本身支不支持结构化输出。如果都做了还不行,就在解析层加兜底,别指望模型 100% 听话。

我踩过的一个坑:提示词里写了“请返回 JSON”,但没写“不要输出解释文字”,结果模型每次都在 JSON 前面加一句“好的,以下是结果”。后来把约束改成“只返回 JSON,第一个字符必须是花括号”,问题解决。

5.2 工具调用选错工具怎么破

九成是工具描述的问题。检查每个工具的描述是不是足够区分。如果两个工具功能相近,考虑合并或加更明确的触发条件。另外,工具数量别太多,超过十个模型就开始犯迷糊。我一般控制在五到八个。

5.3 多轮对话丢上下文

检查状态管理是不是只存了最近一轮。另外注意,工具调用的结果也要存进历史,否则模型不知道上一步执行了什么。我见过一个项目,工具结果没回喂,模型每轮都在重复调用同一个工具。

5.4 响应太慢或 token 消耗过高

先看上下文长度。把历史对话打印出来,往往能发现大量冗余。其次是工具描述太长,精简描述能省不少 token。最后看是不是重试太频繁,重试三次和重试一次的成本差三倍。

问题现象最可能原因排查动作解决方向
格式乱飘约束位置或措辞问题检查约束是否置尾改措辞、加兜底解析
选错工具工具描述模糊对比工具描述细化描述或合并工具
丢上下文状态存储不全打印历史消息补全工具结果回喂
token 爆炸上下文冗余统计各段长度裁剪历史、精简描述
频繁重试解析失败率高看失败日志优化格式约束

5.5 独家避坑技巧

第一个技巧:给模型一个“思考”字段。让它在 action 之前先输出 thought,说明为什么选这个工具。这不仅能提升准确率,排查问题时也能看到模型的决策过程。第二个技巧:工具调用失败时,把错误信息原样回喂,模型往往能自己纠正参数。第三个技巧:定期用固定测试集回归,每次改提示词或工具描述后跑一遍,防止改好一个坏一个。

6. 关于 Harness 工程的一些个人体会

我最初做 AI 应用时,也迷信“模型够强就行”。后来在真实项目里被反复教育,才明白 Harness 这层工程才是决定成败的地方。模型是通用能力,Harness 是把通用能力变成专用能力的转换器。同一个模型,Harness 做得好,能顶上一个专门微调的小模型;Harness 做得差,再强的模型也白搭。

如果你刚开始学 AI 大模型应用开发,我的建议是:先手写一遍最小 Harness,再去看框架。手写的过程会让你对每一层的边界和职责有肌肉记忆。之后用 LangChain、LangGraph 这类框架时,你就知道哪些是框架该管的,哪些必须自己控制。

还有一个体会是,Harness 的迭代是永无止境的。业务在变,模型在升级,工具在增加,Harness 就得跟着调。把它当成一个持续维护的工程资产,而不是一次性的脚手架,心态会完全不一样。我现在的习惯是每次线上出问题,先问一句“这是模型的问题还是 Harness 的问题”,大部分时候答案都是后者。把 Harness 打磨好,模型才能真正为你所用。

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

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

立即咨询