尧图精选

端侧模型用起来难在哪?Harness让Qwen3学会调用工具

🕒 发布时间:2026/9/1 10:31:37 📁 来源:尧图网络
如果在本地把这几年开源的大模型挨个跑一遍你会发现一件很有意思的事把模型跑起来通常只需要一两条命令真正难的是让它在真实任务里“靠谱地工作”。就拿 Qwen3 系列里的端侧版本来说8B、27B 这两档模型被大量开发者拉到自己笔记本和台式机上目标是实现“零成本推理”。但部署完成之后很多人很快发现一个尴尬的事实模型能聊天、能写诗、能做简单问答可让它“查一下系统时间”“算一个表达式”“在代码仓库里搜索某个函数”它就变得非常被动甚至直接输出一段格式错误的 JSON。这里面的差距不在模型本身而在模型外面的那层工程封装——也就是最近社区里反复提到的Harness。从 DeepSeek Harness 到 Codex Harness大家不约而同地意识到同一个问题基座模型决定的是能力上限Harness 决定的是能力能不能兑现。这篇文章不会鼓吹“端侧模型已经可以全面取代云端 API”也不会把 Harness 讲成一个故弄玄虚的框架。我会从实际开发视角把 Harness 到底是什么、为什么端侧模型特别需要它、怎么用 Qwen3 系列 8B/27B 这类模型在本地搭起一套最小可用的推理链路一步步讲清楚。读完你至少能回答一个问题同样是本地模型为什么有些项目能用起来有些项目只能当聊天玩具。1. 零成本推理的真实含义与前置条件“零成本推理”是标题里最吸引人的词也是最容易被误解的词。如果你之前使用云端大模型 API每一轮对话、每调用一次工具账单都会增加。到了月底钱主要花在两类地方一是模型的 Token 费二是把工具链路跑通之后的重复调用费。把模型部署到本地之后单次推理的边际 API 费用确实可以趋近于零这是“零成本推理”这个说法能成立的前提。但注意这不等于一分钱不花。完整成本其实包含四块硬件成本显卡、内存、整机或者一台已有 Mac 的折旧。电力成本8B 档位的模型跑起来还好27B 档位跑满时功耗明显上升。维护成本模型文件下载、量化、推理服务重启、依赖升级。Harness 开发成本这是最容易忽略的一块。模型部署完成后后面接多少工具、上下文怎么管理、任务循环怎么终止都需要花时间写代码。所以更准确的判断是端侧模型不是免掉了成本而是把按调用次数计费换成了前置的一次性投入与运维投入。对个人开发者来说如果手里有现成的 12GB 以上显存显卡或者 16GB 以上统一内存的 Mac把 8B 档位模型跑起来边际成本确实低到可以忽略。但对企业来说所谓“零成本”其实是把成本从“按 Token 付费”变成了“预先付治理成本”。你需要自己管理模型版本、推理服务、权限边界和监控告警。那么为什么会有人愿意花这些成本回到端侧核心不是“免费”而是三点数据不出域。业务数据、代码仓库、隐私资料不会因为调用云端 API 而离开本机。长链路可控。调云 API 时如果某个 Agent 循环失控后端模型不会立刻恢复本地部署则可以通过 Harness 直接中断和回滚。延迟更稳定。相对于公网 API本地推理在稳定网络环境下的一致性更好适合对响应时间敏感的工具链。所以后续章节里所有“零成本”表述都应该在“省掉 API 费用”这个前提下理解。真正的技术重点不是模型怎样被部署起来而是模型部署好之后Harness 怎么让它在任务循环里稳定运行。2. Harness 在端侧模型中的核心作用2.1 什么是 HarnessHarness 英文原意是“马具、缰绳”。在 AI 工程里你可以把它理解成模型与外部世界之间的适配与执行层。大模型本质上是一个文本生成引擎。它接收一段文本输入根据训练得到的概率分布生成下一段文本。这句话听起来朴素但揭示了关键问题模型不会主动调用外部函数不会自己去文件系统里找文件也不会在代码仓库里执行搜索。它只会“生成一段文字”这段文字可能是回答问题也可能是一段 JSON表示它想调用某个工具。要让模型真正做事你需要一个外部系统完成这样的循环把工具描述、可用函数、系统限制一起发给模型。模型根据任务判断是否调用工具。如果模型输出了工具调用意图Harness 解析并执行对应函数。执行结果再塞回上下文让模型基于结果继续推理。重复直到模型给出最终答案或达到最大轮数。这个外部系统就是 Harness。它不负责模型的权重不负责训练只负责让模型“接入真实世界”。2.2 传统调用方式与 Harness 的区别很多开发者第一次接触本地模型时用的可能是最简单的“单轮问答”from openai import OpenAI client OpenAI(base_urlhttp://localhost:8000/v1, api_keyEMPTY) resp client.chat.completions.create( modelqwen3-8b, messages[{role: user, content: 今天几号}], ) print(resp.choices[0].message.content)这种写法能工作但你会发现两个问题第一模型对“今天几号”这类需要实时信息的回答只能靠训练数据里的日期猜测第二如果任务比较复杂比如“先查当前时间再计算 123 乘以 456然后告诉我结果”单轮对话根本无从下手因为模型不会自己把每一步衔接起来。Harness 改变了这个流程。传统 Prompt 调用相当于你把一份工具说明贴在对话前面让模型碰运气Harness 则通过 function calling 协议给模型提供结构化工具定义模型输出结构化的调用请求Harness 解析后执行再把结果回填。两者的对比可以用一张表概括对比项普通 Prompt 调用Harness 封装工具接入把工具说明写进提示词靠模型自行格式化输出走 function calling 协议结构化声明工具结果回传需要自己写正则或 JSON 解析由 Harness 完成 tool_call 回填多轮任务每轮手动拼上下文容易越拼越长循环自动维护消息列表稳定性依赖模型指令跟随能力格式稍变就崩有解析兜底、重试、最大轮数控制适用场景简单问答、文本生成工具调用、代码执行、Agent 任务从社区最近讨论的 DeepSeek Harness、Codex Harness 可以看到一个趋势很多人开始承认Agent 的效果上限不再单纯由基座模型决定而是由 Harness 的质量决定。一个开源模型配上完整工具链、健壮的上下文管理和合理的终止策略能完成的任务复杂度是纯 Prompt 调用无法比拟的。这也是为什么端侧模型不再等于“玩具”的关键转折点。3. Qwen3 8B/27B 的端侧选型思路3.1 为什么选 Qwen3 系列标题里的 Qwen3.8-27B可以理解为 Qwen3 系列中适合端侧部署的 8B 档位和 27B 档位模型。这类开源模型之所以流行有很实际的原因指令跟随能力强针对 Agent 场景做了工具调用相关优化。开源权重可以自由部署适配 Ollama、vLLM、llama.cpp 等主流推理框架。参数规模有梯度8B 档位适合轻量任务27B 档位适合对推理质量要求更高的场景。社区资料多遇到部署问题比较容易找到解决方案。当然具体到某个版本号请以模型官方发布为准。本文的推理链路设计对同量级的开源模型同样适用。3.2 端侧模型选择原则选型不是越大越好。真正要考虑的是四件事显存装得下、上下文长度够用、工具调用稳定、推理速度可接受。先看显存。以常见的 Q4 量化为参考8B 档位模型的权重文件通常在 5GB 左右12GB 显存的显卡已经比较从容27B 档位模型量化后通常不超过 20GB24GB 显存或者 Mac 的 32GB 统一内存在体验上会更稳妥。不同量化策略差距很大实际体积请以你下载的模型文件为准。再看硬件匹配。下面是一个粗略选型表档位量化后权重体积粗略估算推荐硬件适合任务8B 档5GB 左右12GB 以上显存显卡或 16GB 统一内存 Mac工具调用、文本摘要、轻量 Agent27B 档15-20GB24GB 以上显存显卡或 32GB 统一内存 Mac复杂推理、代码补全、多轮任务第三个是工具调用稳定性。8B 模型在简单工具链上表现尚可但工具数量一多或者在复杂上下文里更容易出现“该调用工具却直接生成答案”“格式错误”等问题。27B 档位的稳定性通常更好不过推理速度也更慢。所以实际项目里可以考虑“简单任务走 8B复杂任务走 27B”的双模型路由而不是只靠一个模型打天下。第四个是上下文长度。Agent 任务里每一轮工具调用都会往上下文追加内容如果上下文窗口太小几轮循环后被挤爆Harness 就会进入不稳定状态。部署时建议把 max_model_len 设置成模型支持范围里的一个中间值比如 8192 或 16384而不是直接拉满。4. 本地推理环境搭建Ollama 与 vLLM 两种路线要跑 Harness第一步是先把本地推理服务跑起来。这里有两种主流路线对端侧开发者都比较常见。4.1 路线一Ollama最适合快速验证Ollama 适合第一次接触本地模型的开发者。安装完成后拉取模型并启动服务即可。# 查看本地已有模型 ollama list # 拉取 Qwen3 8B 档位模型具体标签以 Ollama 模型库为准 ollama pull qwen3:8b # 启动交互式对话 ollama run qwen3:8bOllama 较新版本默认会提供一个 OpenAI 兼容端点http://localhost:11434/v1。也就是说你不需要额外写一套调用代码直接用 OpenAI SDK 指向这个地址就行。如果你用的是 27B 档位Ollama 同样能加载只是模型体积更大启动时间更长显存或内存压力也更大。4.2 路线二vLLM适合 GPU 富余和并发场景如果手里的显卡显存比较充足或者你希望后续把 Harness 服务共享给团队使用可以用 vLLM 启动一个 OpenAI 兼容的推理服务。较新版本 vLLM 推荐使用vllm serve命令旧版本对应python -m vllm.entrypoints.openai.api_server二选一即可。下面是等价的一种启动方式vllm serve /data/models/Qwen3-8B-Instruct \ --served-model-name qwen3-8b \ --max-model-len 8192 \ --gpu-memory-utilization 0.85启动后服务默认监听在http://localhost:8000OpenAI 兼容接口路径是http://localhost:8000/v1。4.3 验证推理服务是否就绪无论用哪种路线启动之后都要先确认服务健康。最简单的验证方式是通过/v1/models接口查一下模型列表curl http://localhost:8000/v1/models如果能看到模型返回说明服务已经起来。接下来可以发一个最小对话请求curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen3-8b, messages: [{role: user, content: 11等于几}] }到这里你已经有了一套可用的本地推理后端。下面要做的是把 Harness 接上去。5. 手写一个最小可运行的端侧 Harness这一节我们从一个最小示例出发把 Harness 的核心模块完整跑通。示例会包含三部分本地模型客户端、两个工具函数、一个任务循环控制器。5.1 项目结构与依赖假设项目目录结构如下endpoint-harness/ ├── harness_demo.py └── requirements.txt依赖只需要一个openaiPython 库因为它兼容 Ollama 和 vLLM 提供的接口。# requirements.txt openai1.0安装依赖pip install -r requirements.txt5.2 完整代码下面是harness_demo.py的完整实现。代码设计很克制但该有的模块都有模型接入层、工具注册、上下文管理、任务循环、异常兜底。import datetime import json from openai import OpenAI def get_current_time() - str: 获取当前本地系统时间 return datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S) def calculate(expression: str) - str: 计算数学表达式。 注意这里为了演示简洁使用了 eval仅适合在本地可信环境中运行。 生产环境请用 ast.parse 或白名单方案替代避免任意代码执行风险。 return str(eval(expression, {__builtins__: {}}, {})) class LocalLLM: 本地模型客户端兼容 Ollama 与 vLLM 的 OpenAI 兼容端点。 def __init__(self, base_url: str http://localhost:8000/v1, model: str qwen3-8b, api_key: str EMPTY): self.client OpenAI(base_urlbase_url, api_keyapi_key) self.model model def chat(self, messages, toolsNone, max_tokens512): params { model: self.model, messages: messages, max_tokens: max_tokens, } if tools: params[tools] tools params[tool_choice] auto resp self.client.chat.completions.create(**params) return resp.choices[0].message class Harness: 最小任务循环模型决定是否调用工具Harness 负责执行并回填结果。 def __init__(self, llm: LocalLLM): self.llm llm self.system_prompt ( 你是一个运行在本地设备上的智能助手。 当需要获取实时信息或计算结果时请优先使用工具不要自己猜测。 ) self.messages [ {role: system, content: self.system_prompt}, ] self.tools [ { type: function, function: { name: get_current_time, description: 获取当前本地系统时间, parameters: { type: object, properties: {}, }, }, }, { type: function, function: { name: calculate, description: 计算数学表达式, parameters: { type: object, properties: { expression: { type: string, description: 要计算的数学表达式, } }, required: [expression], }, }, }, ] self.functions { get_current_time: get_current_time, calculate: calculate, } def run(self, user_input: str, max_turns: int 5): self.messages.append({role: user, content: user_input}) for turn in range(max_turns): message self.llm.chat(self.messages, toolsself.tools) print(f[Turn {turn 1}] 模型输出: {message.content or (调用了工具)}) if not message.tool_calls: print(f[Final] {message.content}) return message.content # 把模型的工具调用意图追加回上下文 self.messages.append({ role: assistant, content: message.content or , tool_calls: [ { id: tc.id, type: function, function: { name: tc.function.name, arguments: tc.function.arguments, }, } for tc in message.tool_calls ], }) for tc in message.tool_calls: name tc.function.name try: args json.loads(tc.function.arguments or {}) print(f[Tool] {name}({args})) result self.functions[name](**args) except json.JSONDecodeError: print(f[WARN] 参数解析失败: {tc.function.arguments}) result 工具参数不是合法 JSON请重新调用。 except Exception as e: print(f[WARN] 工具执行失败: {e}) result f工具执行失败: {e} print(f[Tool Result] {result}) self.messages.append({ role: tool, tool_call_id: tc.id, content: str(result), }) print([Reached max_turns]) return None if __name__ __main__: # 如果使用 vLLM默认 http://localhost:8000/v1 llm LocalLLM() # 如果使用 Ollama取消下面这行注释并替换模型名为你实际拉取的标签 # llm LocalLLM(base_urlhttp://localhost:11434/v1, modelqwen3:8b) harness Harness(llm) harness.run(现在几点了顺便帮我计算 123 * 456 的结果。)5.3 关键逻辑说明LocalLLM类负责统一模型调用。它把base_url做成参数这样从 vLLM 切换到 Ollama只需要换地址和模型名业务代码完全不用动。Harness类里最核心的是run方法。它维护一个messages列表每一轮做四件事调用模型、判断是否产生工具调用、执行工具、把工具结果回填。最大轮数max_turns是一个必要的保险丝没有它模型一旦进入“不断调用工具但迟迟不收敛”的死循环整个进程就会被卡住。工具函数的eval是一个明显的安全示例点。演示代码里使用它是为了让你能跑通流程但在生产环境中直接eval用户输入或模型生成的表达式非常危险。实际项目可以把工具换成调用系统 API、读写白名单文件、执行测试用例等更受控的动作。这里真正容易踩坑的地方是很多开发者以为把tools参数传进去模型就一定会使用工具。实际上这取决于推理后端是否支持 function calling也取决于模型本身对工具协议的理解。如果模型总是忽略tools直接给最终答案优先检查推理后端版本和模型格式是否匹配。6. 运行验证如何判断 Harness 真的在工作6.1 运行命令确保本地推理服务已经启动然后执行python harness_demo.py6.2 预期输出如果 Harness 工作正常你应该能看到类似下面这样的流程[Turn 1] 模型输出: (调用了工具) [Tool] get_current_time({}) [Tool Result] 2025-01-15 10:30:00 [Turn 2] 模型输出: (调用了工具) [Tool] calculate({expression: 123*456}) [Tool Result] 56088 [Turn 3] 模型输出: 当前时间是 2025-01-15 10:30:00123 * 456 的结果是 56088。 [Final] 当前时间是 2025-01-15 10:30:00123 * 456 的结果是 56088。这里有两个判断标准模型确实发出了[Tool]调用说明 function calling 链路是通的。[Tool Result
上一篇/下一篇内容由系统自动关联 返回资讯列表 →