资讯动态

DeepSeek Harness实战:从模型接入到Agent Runtime的插件化架构

发布时间:2026/10/16 13:54:14 来源:尧图企业网站定制
我是在一个需要同时对接多个模型服务、并且要快速扩展工具集的 Agent 项目里第一次接触 DeepSeek HarnessDSH的。当时最头疼的是代码里到处是针对模型厂商 API 的硬编码调用每换一家供应商就要把业务逻辑重写一遍更不用说把工具和记忆接进去了。DSH 用一套「一切皆插件」的架构把这个问题拆得非常干净——从最底层的模型接入到上层的 Agent 运行时策略几乎每一个可替换的零件都被抽象成了独立插件。这篇文章会从我实际搭建 DSH 项目的角度一步步拆解它是怎么从 Model 走到 Agent Runtime 的以及为什么这个架构能让你在后期省掉大把改代码的时间。内容会覆盖三部分先讲清楚 DSH 的设计思路再完整演示一个从零跑通的最小项目最后分享我在真实项目中踩过的坑和排查方法。如果你是正在做 LLM 应用或 Agent 项目的开发者哪怕是第一次接触框架类工具看完也能直接照着第三节的代码搭出自己第一个能跑的项目。1. 先想清楚DSH 到底是为了解决什么问题1.1 LLM 应用开发的真实痛点在我用 DSH 之前项目基本是这个状态负责对话的模块里直接写死了一个 chat 请求里面塞了一堆模板字符串工具函数散落在各处有的返回 JSON 字符串有的返回 Python 字典上下文处理靠手动拼列表。刚开始只有一两个模型、四五个工具还能撑住。但当你需要同时调度多个模型、让 Agent 自己决定调用哪些工具、还要在不同环境里复用这套逻辑的时候硬编码方案的维护成本是直线上升的。这套框架的价值恰恰是它从一开始就把这些痛点当作设计前提。你可以理解为传统的 LLM 应用像一个把所有电线焊死在电路板上的设备而 DSH 像是把每一个功能模块做成了标准尺寸的插槽——模型是一种插槽工具是一种插槽记忆是一种插槽连 Agent 的推理策略本身也是一个插槽。你不需要知道插槽背后是什么牌子的零件只需要知道这个插槽能提供什么能力。还有一个容易被忽略的点团队协作。插件化之后负责模型接入的同事只需要维护 Provider 目录下的几个文件负责业务工具的同事在 tools 目录里写代码大家不用在同一个文件里反复合并冲突。模块边界清晰了责任边界也跟着清晰。1.2 一切皆插件到底在说什么第一眼看到 everything is a plugin 这种标语总觉得有点夸大。真正用下来你会发现DSH 的插件化并不是把一段代码用 if-else 包起来就叫插件而是围绕一套明确的接口契约来组织的。你暴露一个类实现几个约定方法注册进去运行时就能在对应阶段调用到你。在 DSH 里几个核心插件类型是这样的ModelProvider负责和具体模型服务交互把模型输入输出统一成标准消息格式。Tool给 Agent 提供可以调用的外部能力本质是一个可被模型理解的函数。Memory管理短期对话上下文和长期知识存储。AgentStrategy决定 Agent 怎么思考下一步比如简单的 ReAct 模式或者更复杂的计划-执行模式。RuntimeHook在运行时关键节点如请求开始、请求结束、工具调用前后插入自定义逻辑。这些插件的组合方式决定了你的 Agent 是什么形态。只装 ModelProvider 加 Tool你得到的是一个类似函数调用助手再加 Memory 和 AgentStrategy就接近一个能自主规划任务的 Agent Runtime。这个设计的本质是关注点分离。模型供应商、工具实现、记忆策略、推理循环是四个完全独立的关注点它们在运行时才被组装。你在开发阶段完全可以用一个模拟的 ModelProvider 测试工具逻辑再在部署阶段换成真正的 DeepSeek 服务。这套思路跟我们在工程里常说的依赖倒置、组合优于继承是一回事只是它把这种思想直接做成了框架的默认形态。如果你之前写过插件系统的代码看到 DSH 的这套约定会非常亲切。2. 从 Model 到 Agent Runtime核心抽象是怎么设计的2.1 Model Provider先把模型服务变成标准电源接口理解 DSH 的第一步是要接受模型本身也是插件这件事。不管你是调用 DeepSeek 的在线 API还是本地跑一个部署模型在 DSH 眼里都是需要适配的电源。它的接口通常长这样class ModelProvider: def chat(self, messages, toolsNone, **kwargs): messages: 标准消息列表每条包含 role、content 等字段 tools: 可选当前会话可用的工具描述列表 返回: 标准响应对象包含模型回复和工具调用请求 ...这个方法接收统一格式的 messages 列表返回标准响应对象。内核不关心你背后是 HTTP 调用还是本地推理只关心你遵守了这个接口。一个关键点是DeepSeek 的 API 本身和业界常见的 chat 接口兼容度很高很多人的第一个 Provider 插件其实就是在原有 SDK 调用外面包一层消息转换。但即使如此我仍然强烈建议不要跳过抽象层因为你在本地调试时用的是一个假 Provider测试完再切到线上——只有通过统一接口才能做到这种切换对业务代码完全透明。我见过一些项目为了省事直接把模型调用写死在业务代码里。三个月后产品要支持新的模型或者要接入公司统一的网关结果就是全工程搜索替换一次替换几十处还要处理各家响应格式的差异。用 Provider 插件之后这种切换基本就是配置文件的改动加上一个新类的事。2.2 Tool 插件给 Agent 装上会说话的手脚Tool 是 Agent 最核心的延伸。Agent 本身不产生工具能力它只负责决定要不要用、用哪个、传什么参数。所以 DSH 里的 Tool 插件本质上是一个带自描述能力的函数。注册一个工具通常要提供三样东西名称、描述、参数 Schema。名称必须短而明确描述则要写清楚什么情况下该调用这个工具参数 Schema 决定了模型能生成多准确的调用。这里有个经验很多新人在工具描述上偷懒结果模型根本不知道该什么时候调或者参数类型对不上。工具描述写得好不好直接决定了 Agent 的可用性这块我在第五节会详细展开。工具被调用后返回的结果会作为新的消息传回模型这个往返过程在 DSH 里是天然支持的。你只需要保证工具返回内容是可序列化的文本或者结构化数据这样模型才能读懂执行结果决定下一步动作。举个例子一个天气工具返回的内容最好是{city: 北京, temperature: 32, condition: 晴, unit: ℃}而不是一段夹杂着日志的字符串。模型对干净的结构化数据敏感度很高你给它一堆噪声它就容易在下一步推理里犯错。2.3 Agent Runtime装配线上的调度中枢如果说 ModelProvider 是电源、Tool 是手脚那 Agent Runtime 就是整个系统的骨架和神经中枢。Runtime 的运行循环大致是observe从会话中取出当前消息和记忆。plan交给 AgentStrategy 插件决定下一步行动直接回答还是调用工具。act执行工具调用拿到结果。reflect把结果写回上下文然后回到 observe直到模型认为任务完成。这个循环对用户是透明的你只需要告诉 Runtime 哪些插件可用它就会自动装配。Runtime 还负责管理会话状态、任务队列、以及插件间的数据传递避免你手动去处理上一个工具的结果传给谁这类琐碎问题。一开始你可能觉得这个循环没什么大不了但真正做复杂任务时就会体会到它的价值。比如你要 Agent 完成查询订单、计算退差价、生成退款申请这样多步骤任务如果没有 Runtime 统一调度你需要自己写状态机处理各种分支。而 DSH 把这一层做好了你要做的只是拆工具、写策略。2.4 Memory 与上下文窗口Agent 稳定的基础Memory 插件在 DSH 里是一个很容易被低估的角色。开发初期你可能觉得上下文不就是把历史消息拼在一起吗——一旦 Agent 多次调用工具或者历史对话变长你就明白为什么需要专门的记忆管理层了。短期记忆在 DSH 里指当前会话内有效的上下文通常由 Runtime 自动维护长期记忆则涉及跨会话的知识沉淀比如读取知识库、用户偏好等。DSH 将这两类能力抽象成 Memory 插件接口后你可以自由替换底层实现——本地用字典存储线上换成向量数据库业务逻辑无需变动。这里要提醒的是别一开始就上重型的向量数据库方案。先用内置的简单 Memory 插件跑通流程确认 Agent 行为和预期一致再考虑升级持久化方案。过早引入重型依赖只会让排查问题的链路变长。而且上下文窗口是有限资源Memory 插件一个很重要的职责是做压缩和裁剪——把不重要的历史摘要化、把过长的工具输出截断这些策略最好先简单后复杂观察模型的实际表现再调优。3. 实操从零搭一个 DSH 项目跑通第一个 Agent3.1 初始化与项目结构假设你已经有了 Python 3.10 环境安装 DSH 很简单pip install dsh安装完成以后可以执行初始化命令让 DSH 为你生成一个骨架项目dsh init my_agent cd my_agent生成的目录结构大致是这样不同版本可能略有差异但核心思想一致my_agent/ ├── dsh.yaml # 配置文件 ├── plugins/ │ ├── providers/ # 模型插件 │ ├── tools/ # 工具插件 │ ├── memory/ # 记忆插件 │ └── strategies/ # Agent 策略插件 └── main.py # 入口脚本这种结构的好处是你不需要在一堆代码里翻找哪段逻辑在哪里每个插件都有固定的位置。团队协作时新人也能根据目录快速理解项目组成。如果你接手过那种几千行单文件的 Agent 项目看到这种目录会觉得格外舒服。3.2 接入 DeepSeek 模型 Provider在 plugins/providers 目录下创建 deepseek_provider.py。DSH 提供一个基础类你继承它、实现核心方法即可from dsh import ModelProvider class DeepSeekProvider(ModelProvider): name deepseek def __init__(self, api_key, modeldeepseek-chat): self.api_key api_key self.model model def chat(self, messages, toolsNone, **kwargs): # 调用 DeepSeek SDK把消息和工具描述传给模型 # 返回 DSH 标准响应对象 from deepseek import client resp client.chat.completions.create( modelself.model, messagesmessages, toolstools, **kwargs ) return self._to_standard_response(resp)这里有个实现细节要注意不同 SDK 返回的对象结构不一样你需要写一个_to_standard_response方法把 SDK 自带的响应对象转换成 DSH 标准格式。转换的核心是提取出模型回复文本和工具调用列表两样东西把它们放进去。然后在 dsh.yaml 里注册这个 Providerproviders: - name: deepseek type: plugins.providers.deepseek_provider.DeepSeekProvider config: api_key: ${DEEPSEEK_API_KEY} model: deepseek-chat注意 api_key 通过环境变量引用不要把密钥写进代码或配置文件里。这个习惯值得一开始就养成后面上线部署能少很多安全方面的麻烦。3.3 写一个自己的 Tool 插件工具插件同样简单。比如写一个获取天气的工具这里用模拟数据避免引入外部 API 依赖import json import random from dsh import Tool, ToolResult class WeatherTool(Tool): name get_weather description 查询指定城市的天气情况适合用户询问温度、降雨、风力等天气信息时使用。 parameters { type: object, properties: { city: {type: string, description: 城市名例如北京、上海}, date: {type: string, description: 日期格式 YYYY-MM-DD缺省为今天} }, required: [city] } def run(self, **kwargs): city kwargs[city] # 模拟返回数据 temp random.randint(0, 35) return ToolResult(datajson.dumps({ city: city, temperature: temp, unit: ℃ }, ensure_asciiFalse))这里的关键是 description 和 parameters 字段。它们不是给程序看的是给模型看的。模型会根据这两个字段判断要不要调用、传什么参数。换个角度想你是在写一份给模型看的 API 文档文档质量直接决定模型调用的准确度。之后在配置文件里注册这个工具plugins: - name: get_weather type: plugins.tools.weather_tool.WeatherTool3.4 组装 Agent Runtime 并运行现在到了把零件拼装起来的时刻。在 main.py 里写from dsh import AgentRuntime from plugins.providers.deepseek_provider import DeepSeekProvider from plugins.tools.weather_tool import WeatherTool runtime AgentRuntime( providerDeepSeekProvider(), tools[WeatherTool()], ) resp runtime.chat(北京今天热吗需要穿短袖吗) print(resp.content)如果一切正常DeepSeek 会判断需要调用 get_weather拿到模拟天气数据后结合数据给你一个带具体温度的建议。第一次看到这个过程完整跑通时你会对模型决定工具调用这件事有很直观的感受模型不是简单地匹配字符串它能理解热不热、穿不穿短袖和查天气之间的语义关联并且自主编排调用。从 Model 到 Agent Runtime 的距离在这一刻就很清楚了。单独调模型你只是拿到了一个聊天窗口把工具和运行时装好后这个聊天窗口突然有了执行能力能自己去查数据再回答。这就是从一个单纯的语言模型到具备任务完成能力的 Agent 之间的关键跃迁。3.5 日志与简单调试DSH 默认会输出运行时日志。我还建议在你的 Provider 实现里加一条工具调用之前和之后的日志打印这样每次交互的完整链路都清晰可见。python main.py --log-level DEBUG打印出来的日志大致包含收到用户消息、模型返回工具调用请求、执行工具 get_weather、工具返回结果、模型生成最终回复。看到这个顺序你的理解就基本到位了。日志不仅用于本地调试生产环境里它也是排查异常的主要线索来源所以从第一天起就不要对日志交互过程这件事掉以轻心。4. 插件系统的关键机制搞懂这几个细节才算入门4.1 插件生命周期加载、注册、卸载DSH 在启动时会对 plugins 目录下的插件做发现与加载。加载过程大致是扫描目录、读取类定义、校验接口签名、实例化并注册到运行时、触发 on_load 钩子。这个生命周期看起来简单实际影响很大。你可以在 on_load 里做资源初始化比如建立数据库连接、加载缓存在 on_unload 里做资源释放。如果某个插件初始化失败DSH 会有明确异常抛出方便你定位是哪个插件的问题。我踩过一个坑插件里定义了一个全局变量以为启动时必执行初始化结果那个变量被延迟加载逻辑跳过运行到一半才报错。后来养成的习惯是任何需要用的资源都在 on_load 里显式初始化不依赖类的默认字段。还有一次是在 on_load 里连数据库连接失败后进程直接退出排查了半天才发现是依赖的服务没启动。所以插件初始化阶段一定要做友好失败——把错误信息写清楚而不是抛一个裸异常。4.2 配置与依赖注入每个插件在注册时都会拿到一份配置对象。DSH 采用分层配置合并默认值、环境变量、外部传入三层依次覆盖。这个逻辑和主流框架的配置管理机制很像但更轻量。比如你的工具需要调用某外部服务的地址可以写在 dsh.yaml 里也可以让环境变量覆盖tools: - name: query_order config: endpoint: http://internal-order-service # endpoint 也可以改成 ${ORDER_SERVICE_URL}插件代码里直接从注入的 config 里读取不关心配置来自哪一层。这样你在本地开发、测试环境、生产环境跑同一套代码只需要改变环境变量代码完全不动。依赖注入还体现在插件之间的调用上。DSH 不会让工具插件直接去 import 另一份 Provider 实例而是通过 Runtime 提供的能力接口来获取。这样每个插件都是无状态的组件状态集中在 Runtime 统一管理测试时你可以很方便地替换任何一个依赖。4.3 数据流与上下文传递整个 Agent 运行过程核心数据是 Message 对象。它内部包含 role、content、tool_calls、tool_call_id 等字段正好对应模型 API 的请求结构。工具调用过程中消息会经历三种形态用户消息{role: user, content: 北京今天多少度}模型返回的工具调用请求{ role: assistant, content: None, tool_calls: [ { id: call_001, function: {name: get_weather, arguments: {\city\: \北京\}} } ] }工具执行后回传的结果消息{role: tool, tool_call_id: call_001, content: {\city\: \北京\, \temperature\: 32}}看到这里你就明白了工具结果必须通过 tool_call_id 和之前的调用请求关联起来模型才能正确理解这次返回对应的是哪次调用。如果你在手动拼接上下文时漏掉了这一步模型很可能会犯迷糊产生幻觉或者重复调用。上下文传递的另一个重要概念是会话标识。DSH 在每次会话开始时生成一个唯一标识贯穿整个调用链。排查问题时你只需要按照这个标识把日志聚拢就能看到一次完整的 Agent 轨迹。这个设计对生产环境几乎必不可少建议一上手就关注。5. 实战中容易踩的坑附排查速查表5.1 工具描述写得太烂模型根本不调用这是新手踩得最多的坑。下面这个描述就是典型的反例{ name: get_weather, description: 获取天气数据, parameters: { type: object, properties: {} } }模型看到这种描述根本不知道获取天气数据到底适合什么场景、需要什么参数。而像下面这种{ name: get_weather, description: 根据城市名称和日期查询天气预报当用户询问温度、降雨、风力等天气情况时使用。返回值为实时天气数据和 24 小时预报。, parameters: { type: object, properties: { city: {type: string, description: 城市名例如北京、上海}, date: {type: string, description: 日期格式 YYYY-MM-DD缺省为今天} }, required: [city] } }模型就能很准确地匹配用户意图。经验法则是描述里应该包含什么时候用、有哪些参数、返回什么三要素。你甚至可以拿几个典型问题测一下模型看它是否会按你的预期触发工具。我见过团队里一个查询订单工具描述里只写了查询订单结果模型在用户问我的快递到哪了时死活不触发改成根据订单号或手机号查询物流状态当用户询问快递、物流、发货进度时使用之后立刻就好了。5.2 模型端接口调整与参数不兼容模型服务方的接口偶尔会调整比如新增了推理参数、调整了字段命名。如果你的 Provider 插件里写死了某些参数名服务端升级后可能直接抛异常。我的做法是把模型相关配置模型名、温度、最大 token 等全部外置到配置文件中代码里只透传参数不写死。这样即使模型端更新通常只需要改配置而不是改插件代码。另外一个相关的问题是重试逻辑。在线模型服务偶尔会超时或者返回限流错误你在 Provider 层就要做好重试和退避。我建议把重试次数和退避时间也放在配置里不要写死在代码中。5.3 工具调用重复执行与幂等设计在 Agent 循环里同一个工具可能会被模型多次调用。如果这个工具不是幂等的——比如说触发发送短信、创建订单、扣减库存——就会出事故。一个简单的防御办法是在工具接口里支持请求唯一标识Runtime 在重试时带上同一个标识工具内部做去重。DSH 本身提供了一些去重的钩子但最终还是要你在业务侧保证幂等性。我有一次测试中Agent 因为上下文被截断连续两次调用了发送验证码这个工具用户收到了两条短信。排查之后发现问题不在 DSH而是我在工具里没有做防重。加上一个按手机号和验证码类型做的三分钟去重之后这个问题就消失了。涉及真实系统操作的工具建议一律按可能被重复调用来设计。5.4 问题排查速查表现象可能原因排查方法模型从不调用任何工具工具描述不够明确或者模型不支持工具调用检查 description 是否包含触发场景用模拟工具验证模型调用工具但参数总传错参数 Schema 缺少类型和字段描述使用详细 JSON Schema每个字段写明含义和示例模型反复调用同一个工具上下文被截断或工具结果未正确回传确认 tool_call_id 和 roletool 消息是否进入上下文工具执行成功但模型回答不相关工具返回数据格式混乱噪声太多统一工具返回为干净 JSON不要混入日志文本切换模型后行为明显变差Provider 转换层丢失了部分参数检查响应转换是否完整把模型差异隔离在 Provider 内并发请求时状态错乱插件内部使用了不安全的全局变量无状态化插件把可变状态交给 Runtime 管理6. 从入门到进阶DSH 还能怎么玩6.1 多 Provider 路由与自动降级把 Provider 做成插件后多模型路由变得非常自然。你可以配置两个 Provider比如 DeepSeek 负责主问答另一个作为兜底。Runtime 在检测到主 Provider 超时或返回异常时自动切换到备用 Provider。这个能力在生产环境的价值很大模型服务偶尔抖动时用户几乎无感知。配置层面大致是这样routing: - provider: deepseek weight: 90 - provider: fallback_model weight: 10当然真正的路由规则可以更复杂比如按请求类型路由、按成本路由。只要你的路由策略是一个插件DSH 就能把它接入。我目前用的是主备 健康检查的模式主 Provider 连续失败三次就临时摘掉过五分钟再探测恢复。刚开始做多 Provider 路由时不要追求复杂的负载均衡策略。最简单的权重路由加上自动降级已经能覆盖绝大多数场景。复杂的策略往往带来新的故障点不如先把基础的稳定性做扎实。6.2 用 Runtime Hook 实现可观测性除了核心插件我强烈建议你尝试写一个 RuntimeHook 插件。它可以在每个请求阶段打印耗时、token 消耗、工具调用次数等指标。这些指标在本地 demo 时觉得无所谓一旦做线上服务就变成了判断系统健康度的关键数据。一个简单的耗时统计 Hook 只需要几十行代码在工具调用开始阶段记录开始时间在工具调用结束阶段计算耗时并输出日志。把这些指标聚合上报到监控系统你就能画出整个 Agent 链路的性能画像。import time from dsh import RuntimeHook class TimingHook(RuntimeHook): def on_tool_call_start(self, ctx): ctx.set(tool_start_time, time.time()) def on_tool_call_end(self, ctx): start ctx.get(tool_start_time) print(fTool {ctx.tool_name} took {time.time() - start:.2f}s)这类横切关注点用 Hook 插件实现业务代码可以保持干净。6.3 从本地测试走向生产部署的注意点本地 demo 跑通后离生产还有几步路要走。一是接入统一鉴权不能让每个调用方直接持有模型 API Key。二是在 Runtime 外侧加超时和并发控制防止工具层阻塞拖垮整体服务。三是把会话标识上报到日志平台方便后续排查。这三件事看起来和 Agent 本身无关但恰恰是生产级 Agent Runtime 和玩具 demo 的分水岭。DSH 插件化的好处在这里再次体现——这些横切关注点都可以通过 Hook 插件优雅插入而不需要改动你的 Agent 业务核心代码。我自己在部署第一个在线 Agent 服务时最深的感受就是模型能力只占一半另一半是工程稳定性。无论是模型超时、工具异常还是上下文溢出都需要你提前设计好兜底策略。而 DSH 这套插件化架构至少让你在做这些设计时不用先花时间重构代码。我个人在实际操作中的体会是DSH 这类一切皆插件的框架真正节省的不只是切换模型时的那几天开发量它最大的回报是让你在架构思路上保持清晰。插件边界一旦划好团队的协作方式也会跟着改变——负责模型的改 Provider负责工具的实现 Tool负责业务逻辑的关注 Runtime 组装大家互不踩脚。如果你打算在自己项目里尝试这套思路我的建议是不要一开始就追求插件的数量和复杂度先用最精简的 Model 加一个 Tool 跑通完整链路感受模型决定、运行时执行这个核心循环再逐步加入 Memory、Hook、多模型路由。插件化不是目的把复杂系统变可组合、可替换才是目的。希望这篇入门能帮你少走一些弯路把精力省下来真正放到 Agent 业务价值的打磨上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑