
1. 为什么说 Agent 不只是 Model Tools1.1 Model Tools翻车现场一次真实的小实验我常跟人说Agent开发最害人的一句话就是Agent就是模型加一堆工具函数。这句话从理论上讲没错但凡是亲自写过的人大概都经历过这样的场景你给Agent挂上一个查天气的工具模型也很听话地生成了对应的参数一切看起来顺风顺水。等到工具真正执行的时候真实返回的结果往往不是一个乖乖的JSON而是一段带HTML标签的页面、一段日志、甚至是一条空错误信息。模型拿到这些原始返回值之后反而开始胡言乱语把查询失败理解成天气晴朗。我自己做过一个最小的实验让模型调用一个工具工具返回的是成功和失败两种状态但返回内容里夹了一段备注文字。结果模型把备注当成了工具结果的一部分据此给出了完全错误的判断。这不是模型不够聪明而是模型和工具之间缺了一层神经系统——把工具结果规范化、把错误信息结构化、把调用过程控制住的那个环节。这个环节就是Harness。1.2 HarnessAgent 的驾驶舱而不是工具箱用一个好懂的比喻如果模型是汽车发动机工具是方向盘、油门、刹车那Harness就是驾驶舱里的仪表盘、控制系统和行车电脑。发动机再好没有仪表盘你也不知道转速多少、油量多少没有控制系统方向盘和刹车也只是有而已并不能真正按照你的路线行驶。在我整理的DeepSeek Harness架构里Harness要负责的事情远比调一堆函数多会话上下文管理历史消息怎么存、超长对话怎么截断、关键信息怎么保留工具注册与发现模型怎么知道有哪些工具可用、参数结构是什么、哪些工具当前被禁用模型输出解析模型返回的内容不一定是合法JSON可能有注释、可能有前后缀、可能参数缺项要统一规整执行循环模型说我要调工具A之后框架要真正去执行A并把结果拼回上下文再交给模型继续思考错误处理与重试工具超时、模型限流、上下文超长都需要有兜底策略多智能体编排多个Agent不只是套娃式地互相调用还需要路由、仲裁和上下文隔离。这套东西合在一起才算是一个能落地的Agent。很多人只看到了Model和Tools这两个明显组件却忽略了它们之间的粘合层于是项目一复杂就开始失控这也是我为什么想把这些经验完整地写成一本系统性的手册——市面上讲怎么调模型的文章很多讲怎么把Agent扣成产品的太少。2. DeepSeek Harness 的核心设计拆解2.1 从一次性脚本到可复用框架Skill、Tool、Agent三层设计我最开始写的Agent就是一个while循环加json解析跑通第一个工具时还挺兴奋。但第二个、第三个工具加进来代码马上变成了意大利面条if-else满天飞参数解析到处都是模型一旦返回意外格式就得重写。踩了几次坑之后我决定把结构沉淀下来往框架方向走一步也就有了书里花了大量篇幅去讲的Harness三层模型。现在的项目我基本都会分成三层Tool层无状态的原子能力。比如获取当前时间执行一段Python代码查询某个知识库条目。Tool只负责一件事输入输出都有明确的JSON Schema约束不做业务决策。Skill层有状态的工作流。一个Skill可能由多个Tool组合而成还带有自己的提示词和中间步骤。比如代码审查这个Skill它需要拿Diff、逐文件分析、汇总成报告这不是某个Tool能完成的。Agent层带目标的策略主体。Agent组合Tools和Skills负责规划下一步做什么、怎么判断任务完成、遇到失败怎么调整。很多朋友经常问Skill和Agent的区别我的回答很简单Skill是做事的能力Agent是做决策的主体。同一个Skill可以被不同Agent复用同一个Agent也可以按任务流程切换多个Skill。书里我专门画了一张表把Tool、Skill、Agent在是否持有状态是否包含提示词是否可独立运行是否可被编排四个维度上做了对比写代码之前先把这张表理清楚后面会少走很多弯路。2.2 上下文管理决定 Agent 上限的隐藏变量在DeepSeek Harness里我最看重的模块是上下文管理。模型的上下文窗口是有限的但对话是无限长的工具结果可能非常大。如果不做管理跑着跑着就会撞到maximum context length报错那基本等于Agent当场瘫痪。我现在的做法是给上下文做一个预算表系统提示词一般控制在1500 token以内只放角色、规则、工具清单摘要最近的消息完整保留这是当前推理最需要的信息更早的历史消息压缩成摘要摘要也设一个上限工具返回值在返回给模型之前先截断单条结果超过2000 token就只保留关键字段。这里有一个我踩过的坑不要把所有历史都塞给模型也不要太激进地只留最后几条。很多Agent任务需要回溯前面对话里的承诺或结论所以摘要不是简单删除而是要把已完成的动作、当前目标、未解决的问题三件事写清楚。这比任何花哨的向量检索都好用因为摘要本身是模型生成的天然契合模型的语义空间。2.3 工具调用的三个高发问题参数、返回值、重试工具调用最容易翻车的环节有三个。第一个是参数生成不规范。模型返回的tool_calls里参数有时候是JSON字符串有时候前后多了代码块标记有时候字段名跟Schema对不上。我一般不做太复杂的容错——先把参数剥离成纯JSON再交给pydantic校验校验不过就生成一条结构化的参数错误消息喂回给模型让它自己修正。这里千万不要怕多问一次模型多一次交互的成本远低于你写一堆正则去硬解析。第二个是返回值不规整。这是翻车重灾区。工具是给程序用的不是给模型看的所以工具内部一定要做一次面向模型的结果格式化。比如抓取网页内容后不要把整段HTML塞回去而是提取标题、段落、关键列表统一包一层JSON格式。书里我引用过一个很形象的类比你请了一个实习生帮你查资料实习生抱着一摞原件给你你看完头晕你要的是他先整理成三行摘要再汇报。第三个是超时与重试。工具执行可能卡住模型接口可能限流网络波动更是家常便饭。我的Harness会给每次模型调用和工具调用设置超时时间失败时用指数退避重试三次连续失败的工具临时从可用列表里移除避免模型反复选同一个坏工具。这事不做线上跑起来早晚有一天会被一条慢接口拖死。3. 实操从零搭建一个 DeepSeek Harness 最小可运行骨架3.1 环境准备与版本选择书里的实操部分我用的是最稳妥的组合Python 3.10以上、OpenAI Python SDK作为客户端。很多朋友一听说要用DeepSeek就以为得单独装一套SDK其实不用DeepSeek的接口兼容OpenAI协议你只需要在客户端里把base_url和api_key换成自己的就行。依赖装起来非常轻这也是我把这部分放在全书第一章的原因——先把能跑通的环境准备好再谈架构。python -m venv .venv source .venv/bin/activate pip install openai pydantic python-dotenv在项目根目录放一个.env文件写上你的API Key和模型名称。这里我强烈建议把模型名也放进配置里不要写死在代码里因为做多模型兜底的时候你就知道这份敬畏心有多重要了。DEEPSEEK_API_KEYsk-xxxx DEEPSEEK_MODELdeepseek-chat DEEPSEEK_BASE_URLhttps://api.deepseek.com/v1装好之后别急着写复杂逻辑先跑一个最简单的对话请求确认环境通不通。我见过太多人一上来就装各种Agent框架结果环境冲突折腾一整天最后发现核心问题只是Key没配对。最小闭环永远是最优先的事。3.2 核心执行循环一个极简 Runner下面这个Runner是我所有Harness的原型它做的事情很简单维护消息列表调用模型如果模型要求调用工具就执行并返回结果重复直到模型给出最终答案。整段代码不到四十行但已经把模型工具那个循环的骨架讲透了。import json import os from openai import OpenAI client OpenAI( api_keyos.environ[DEEPSEEK_API_KEY], base_urlos.environ[DEEPSEEK_BASE_URL], ) def run_agent(tools, user_prompt, max_iterations8): messages [ {role: system, content: 你是一个务实的助手。需要调用工具时请严格按工具JSON Schema给出参数。}, {role: user, content: user_prompt}, ] for _ in range(max_iterations): response client.chat.completions.create( modelos.environ[DEEPSEEK_MODEL], messagesmessages, toolstools, temperature0.3, ) message response.choices[0].message if not message.tool_calls: return message.content messages.append({ role: assistant, tool_calls: [ { id: tc.id, type: function, function: { name: tc.function.name, arguments: tc.function.arguments, }, } for tc in message.tool_calls ], content: message.content or , }) for tc in message.tool_calls: fn_name tc.function.name args json.loads(tc.function.arguments) result execute_tool(fn_name, args) messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps(result, ensure_asciiFalse), }) raise RuntimeError(达到最大迭代次数任务未收敛)这段代码最关键的边界细节是assistant消息里必须带上tool_calls原始结构后续每条tool消息必须用tool_call_id关联到对应的调用。很多人自己拼消息时漏了这两点模型接口会直接报出invalid tool call之类的错误。这个坑我至少见过十几次每次都是在消息结构对不上的问题上卡了一两个小时。3.3 工具注册表用装饰器把函数变 Tool最小跑通之后第二步就是把工具做结构化。我习惯用一个注册表函数通过装饰器自动生成工具的JSON Schema。这样工具的定义和使用是分离的新增工具只需要写一个函数再挂一个装饰器不需要改任何调用方代码。TOOL_REGISTRY {} def tool(name, description, schema): def decorator(func): TOOL_REGISTRY[name] { type: function, function: { name: name, description: description, parameters: schema, }, fn: func, } return func return decorator tool( get_current_time, 获取当前本地时间, { type: object, properties: {format: {type: string, enum: [iso, human]}}, }, ) def get_current_time(formatiso): from datetime import datetime now datetime.now() return {ok: True, time: now.isoformat() if format iso else str(now)}执行工具时不要直接调用函数而是先做参数校验再包一层异常处理。我的execute_tool差不多长这样def execute_tool(name, args): spec TOOL_REGISTRY.get(name) if spec is None: return {ok: False, error: f未知工具: {name}} try: result spec[fn](**args) return {ok: True, **result} except Exception as e: return {ok: False, error: f{type(e).__name__}: {str(e)}}这样设计的好处是模型永远拿到结构化的成功或失败信息它可以根据error字段自行调整参数或换一个工具继续尝试。我在写书过程中反复验证过这种错误即信息的思路比让框架直接抛异常要可靠得多。Agent是自主纠错的系统你越早把错误转成它看得懂的信息它就越早从错误中恢复。3.4 多智能体编排为什么不能简单套娃单个Agent跑通后大家都会想上多Agent。我的经验是多Agent编排的复杂度是单Agent的平方以上不要轻易玩但真需要的时候要清楚编排模式和它的代价。最简单的编排模式是Supervisor一个主Agent负责任务拆解把子任务分给专用Agent再汇总结果。这个模式实现不难但token开销很夸张主Agent每次汇总都要带着各子Agent的返回全文上下文很容易爆掉。我在Harness里会要求子Agent的结果必须压缩成不超过500 token的结构化报告否则主Agent就会被噪声淹没。另一种模式是Pipeline任务顺序经过多个Agent每个Agent只负责一个环节。这个模式的好处是上下文隔离每个环节的输入输出都很清晰坏处是一旦中间环节出错后续环节全部白跑。我在实际项目里更常用Pipeline因为它好调试出问题能定位到具体环节。无论哪种模式都必须有一套防循环机制。多Agent互相调用时A把任务抛给B、B又抛回给A的情况非常常见。我给每个子任务设置了最大转发次数默认3次超过就强制结束并把未能解决作为一个正常结果返回。不要小看这个机制没有它你的编排层会在某个诡异的边界case里无限空转最后白白烧掉大量调用额度。4. 写书过程中沉淀的避坑清单与排查实录4.1 高频报错selected model is at capacity有段时间我连续遇到selected model is at capacity. please try a different model一开始误以为是自己请求写错了后来才发现这是模型服务侧的负载问题跟你代码没关系。应对方式不复杂在Harness里做模型级重试失败后指数退避比如第一次等1秒、第二次等3秒、第三次等7秒还可以配置一个备用模型把当前模型不可用当成一个可恢复错误而不是直接抛给用户。很多人迷信换个更聪明的模型能解决Agent问题但这类基础设施级的抖动靠的是工程手段而不是模型能力。4.2 高频报错maximum context length is 1048576 tokens这个报错我见到过好多次尤其当工具返回大量数据或Agent对话轮次很多时。它的本质是上下文预算失控。我建议在每次请求之前先估算当前messages的总token数超出阈值就执行压缩策略优先丢弃最早的历史并生成一段摘要顶替。工具结果尤其要警惕你在工具里console.log出来的调试信息如果不加限制全部会被当成上下文送进模型这是最大的隐性消耗。实际项目中一条工具结果撑爆上下文的情况占比极高截断工具返回是投入产出比最高的优化。4.3 高频报错Agent execution terminated due to error这个报错常见于某个工具执行抛了未捕获异常。我早期代码里工具函数直接抛Exception整个循环就死了。后来所有工具执行全部改成返回结构化错误框架层面不抛异常模型拿到错误后会自己纠错。这一条看起来简单却是我整个Harness稳定性提升最大的一个改动。改完之后线上Agent的自动恢复成功率从不到五成涨到了九成以上而且你再也不需要半夜爬起来看日志了。4.4 高频报错环境类报错与项目报错要分开查写书期间有不少读者来问我一些看起来和Agent完全无关的报错比如安装脚本在虚拟机里运行失败。这种报错多半是环境依赖版本不匹配、权限不够或者系统组件未加载跟Agent业务逻辑没有关系。排查思路就一条不要看泛化错误文案去翻具体日志找到真正报错的那一行再搜解决方案比对着报错全文硬排有效得多。这也算是一个通用的排障习惯你把它用在任何技术栈上都不会错。4.5 避坑速查表我把高频问题和对应策略整理成一个表写书时就是按这个结构组织的症状常见原因有效对策selected model is at capacity模型服务侧负载高指数退避重试 备用模型maximum context length上下文预算失控历史摘要压缩 工具结果截断execution terminated due to error工具抛未捕获异常统一返回结构化错误invalid tool call消息缺tool_call_id或结构不完整严格按协议拼消息模型反复选坏工具未标记工具失败状态连续失败后临时禁用工具多Agent互相踢皮球缺少转发限制子任务最大转发次数 超时强制结束工具返回HTML杂讯未做面向模型的结果格式化提取关键字段并统一JSON封装这张表我在每次写新的Agent骨架时都会翻出来对照很多问题不是靠换个更聪明的模型解决的而是靠把这些边缘情况处理干净。我写这本书的起因其实是一次线上Agent事故——工具把整页HTML塞给了模型模型一本正经地分析HTML里的嵌套标签得出了一个完全离谱的结论。那一刻我突然意识到Agent真正的胜负手不是模型参数也不是工具数量而是它们之间那层没人愿意花时间设计的Harness。如果你正准备上手Agent开发我的建议是别急着搭什么宏大框架。先用三五十行代码跑通一个最小循环挂一个最普通的工具把上面说的参数校验、返回值规整、超时重试都过一遍再去考虑多Agent编排。当你把这一层跑扎实了你自然会理解为什么我说Agent从来都不只是Model加Tools——它还需要一个把两者拧成一股绳的Harness。最后再分享一个小技巧给工具返回值做面向模型格式化时可以在每条结果前面加一句简短的自然语言说明比如查询成功以下是最新的两个条目而不是直接抛一个JSON对象。模型对夹在对话里的自然语言解释理解得反而更准确这个小改动几乎零成本却能让工具调用稳定性上一个台阶。