
1. 从模型选型到生产级智能体为什么我最终选了 Hermes 这套组合过去大半年我一直在折腾 Agent 工程落地这件事。从最早的纯 Prompt 编排到后面接 Function Calling再到把模型换成 Hermes 系列、用 vLLM 做推理后端中间踩的坑比写过的代码还多。这篇文章不讲概念只讲我实际跑通的一套方案Hermes 模型 Function Calling Python vLLM从选型逻辑一路讲到生产级智能体的完整代码实现。先说清楚这套东西是干什么的。Hermes 在这里指的是具备强 Function Calling 能力的开源指令模型系列它在工具调用、结构化输出、多轮对话上的表现是我试过的开源模型里比较稳的一档。Agent 则是把这套模型能力包装成一个能自主决策、调用工具、维护记忆、循环执行任务的智能体。Function Calling 是模型和外部世界之间的那根“电话线”Python 是胶水语言vLLM 是让模型跑得快、扛得住并发的推理引擎。这套方案适合谁如果你已经会一点 Python想从“调 API 玩一玩”进阶到“自己部署一个能上生产的 Agent”那这篇就是给你写的。如果你是完全的新手也没关系我会把 Python 环境、vLLM 部署、Function Calling 的细节都拆开讲保证你能跟着复现。整条链路我实测下来是稳的下面把每个环节的取舍和代码都摊开说。2. 整体架构设计与选型逻辑拆解2.1 为什么是 Hermes而不是随便找个模型选模型这件事很多人一上来就看榜单分数这是最容易踩的坑。Agent 场景和聊天场景完全是两码事。聊天场景看的是语言流畅度和知识广度Agent 场景看的是三件事指令遵循、结构化输出稳定性、工具调用准确率。我早期用一些通用对话模型做 Agent最头疼的就是它“不听话”。你让它输出 JSON它给你输出一段带解释的自然语言你让它调用工具它把参数拼错或者干脆自己编一个不存在的函数名。Hermes 系列在这方面的训练是专门强化过的尤其是 Function Calling 的格式遵循实测下来在同样的 Prompt 下工具调用成功率明显更高。另一个关键点是可控性。生产环境里模型的行为必须是可预测的。Hermes 的指令微调风格比较“务实”不太会自作主张地加戏这对 Agent 的稳定性至关重要。你不需要它有多聪明你需要它在该调用工具的时候老老实实调用在该返回结果的时候干净利落地返回。2.2 vLLM 作为推理后端解决的是什么问题模型选好了怎么跑起来是第二个大问题。直接用 transformers 库加载模型单条推理还行一旦并发上来显存和吞吐立刻崩。vLLM 的核心价值在于PagedAttention和连续批处理Continuous Batching。打个比方传统推理就像餐厅一次只能服务一桌客人这桌吃完才接待下一桌vLLM 的连续批处理则是流水席谁吃完了立刻补新客人进来GPU 利用率能拉满。PagedAttention 则是把显存里的 KV Cache 像操作系统管理内存分页一样管理避免了大量碎片浪费。实测下来同样的显卡vLLM 的吞吐能比朴素实现高出好几倍这对 Agent 这种需要频繁多轮调用的场景是刚需。而且 vLLM 提供了 OpenAI 兼容的 API 接口这意味着你的 Agent 代码可以用标准的 OpenAI SDK 去调用本地模型迁移成本极低。这一点非常关键后面代码里你会看到我几乎没改多少东西就把云端调用切成了本地部署。2.3 Function Calling 在 Agent 里的角色定位很多人把 Function Calling 理解成“让模型调用函数”这个理解太浅了。Function Calling 的本质是给模型一套结构化的行动空间。模型本身不能执行任何操作它只能输出文本。Function Calling 做的事情是把“我想查天气”这种意图翻译成{name: get_weather, arguments: {city: 北京}}这种机器能执行的指令。在 Agent 循环里Function Calling 是决策和执行的分界线。模型负责决策选哪个工具、传什么参数你的 Python 代码负责执行真正去调 API、查数据库、发请求然后把结果喂回给模型让它继续决策。这个循环就是 Agent 的心跳。设计工具集的时候有个原则工具要原子化不要贪大。我见过有人设计一个do_everything的工具参数一大堆结果模型根本用不明白。正确的做法是把每个能力拆成独立的小工具参数尽量少、类型尽量简单让模型容易理解和选择。2.4 整体数据流长什么样把上面几块拼起来整个 Agent 的数据流是这样的用户输入进来Python 层把系统提示词、历史对话、可用工具列表一起打包成请求发给 vLLM 上的 Hermes 模型。模型返回一个响应如果响应里包含工具调用Python 层就解析出来、执行对应函数、把结果作为一条新消息追加到对话历史里再次请求模型。这个循环一直持续到模型返回纯文本的最终答案为止。这个循环里有两个容易出问题的地方一是循环终止条件必须有最大轮次限制否则模型可能陷入死循环二是上下文长度管理多轮工具调用会让对话历史迅速膨胀需要做截断或摘要。这两点后面会详细讲。3. 环境搭建与 vLLM 部署实操3.1 Python 环境准备别在这步浪费时间Python 环境这块我的建议是直接用 conda 或者 venv 建独立环境别在系统 Python 上瞎装。版本选 3.10 或 3.11这两个版本对主流库的兼容性最好。3.12 有些库还没跟上容易出幺蛾子。conda create -n agent python3.11 -y conda activate agent装依赖的时候核心就几个openai用它的 SDK 调 vLLM 的兼容接口、requests、pydantic做参数校验。如果你要做量化交易或者爬虫类的工具再按需装pandas、sklearn、beautifulsoup4这些。pip install openai requests pydantic提示pip 装包慢的话换个国内镜像源别硬等。但注意别把镜像源配到全局容易污染其他项目。3.2 vLLM 部署 Hermes 模型Docker 是最省心的路子vLLM 的安装方式有两种pip 直接装和 Docker。我强烈建议用 Docker尤其是你要上生产的时候。原因很简单vLLM 对 CUDA 版本、PyTorch 版本、驱动版本非常敏感pip 装很容易出现版本冲突Docker 镜像把这些依赖都锁死了省心。拉镜像的时候注意版本号不同版本的 vLLM 对模型的支持不一样。我用的镜像是vllm/vllm-openai系列这个镜像自带 OpenAI 兼容的 API server启动即用。docker run --gpus all \ -v /path/to/models:/models \ -p 8000:8000 \ --shm-size 16g \ vllm/vllm-openai:latest \ --model /models/Hermes-xxx \ --served-model-name hermes \ --max-model-len 8192 \ --gpu-memory-utilization 0.9这里几个参数值得说道说道。--max-model-len控制最大上下文长度设太大显存扛不住设太小多轮对话会截断8192 是个比较平衡的值。--gpu-memory-utilization 0.9表示让 vLLM 用 90% 的显存留一点给系统设成 1.0 有时候会 OOM。--shm-size是共享内存Docker 默认的 64M 太小多进程推理会崩给到 16G 比较稳。注意Docker 镜像本身不带模型权重模型文件要自己下载好挂载进去。别指望镜像里啥都有那是想多了。3.3 验证服务是否正常服务起来之后先用 curl 测一下别急着写 Agent 代码。curl http://localhost:8000/v1/models能返回模型列表就说明服务正常。然后再测一下对话接口curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: hermes, messages: [{role: user, content: 你好}] }如果这两步都通了说明推理后端没问题可以进入 Agent 开发了。如果报错八成是显存不够或者模型路径不对先看日志。3.4 显存不够怎么办几个实用的降级方案不是每个人都有 A100。如果你显存紧张有几个办法。第一是量化用 AWQ 或 GPTQ 量化版本的模型显存占用能降到原来的三分之一左右精度损失在可接受范围内。第二是调小--max-model-len上下文长度和显存是线性关系。第三是--tensor-parallel-size多卡并行但这个需要多张卡。我实测下来一张 24G 的卡跑 7B 级别的 Hermes 模型用 AWQ 量化max-model-len设 8192是比较舒服的配置能扛住一定并发。4. Function Calling 与 Agent 核心代码实现4.1 工具定义把能力描述清楚是成功的一半工具定义用的是 JSON Schema 格式这是 OpenAI 兼容接口的标准。每个工具要有名字、描述、参数定义。描述这块千万别偷懒模型就是靠描述来判断该不该用这个工具的。tools [ { type: function, function: { name: get_weather, description: 查询指定城市的实时天气。当用户询问天气相关问题时使用。, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海 } }, required: [city] } } } ]描述里我特意写了“当用户询问天气相关问题时使用”这是在给模型划边界。工具描述写得越明确模型误用的概率越低。参数描述也要写清楚格式比如城市名称给个例子模型就知道该怎么填。4.2 工具执行层真正干活的地方模型只负责说“我要调 get_weather”真正执行的是你的 Python 代码。这一层要做两件事把模型给的参数解析出来调用对应的函数然后把结果返回。import json def execute_tool(tool_call): name tool_call.function.name args json.loads(tool_call.function.arguments) if name get_weather: return get_weather(**args) else: return json.dumps({error: f未知工具: {name}}) def get_weather(city): # 实际项目里这里调真实天气 API return json.dumps({city: city, temp: 25°C, condition: 晴})这里有个坑要注意模型返回的arguments是字符串不是字典必须json.loads一下。而且模型偶尔会返回不合法的 JSON所以最好加个 try-except 兜底解析失败就返回错误信息让模型重试。4.3 Agent 主循环心跳逻辑这是整个 Agent 的核心。逻辑不复杂就是不断地请求模型、执行工具、把结果喂回去直到模型不再调用工具为止。from openai import OpenAI client OpenAI(base_urlhttp://localhost:8000/v1, api_keyEMPTY) def run_agent(user_input, max_turns10): messages [ {role: system, content: 你是一个智能助手可以调用工具来完成任务。}, {role: user, content: user_input} ] for turn in range(max_turns): response client.chat.completions.create( modelhermes, messagesmessages, toolstools, tool_choiceauto ) msg response.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content for tool_call in msg.tool_calls: result execute_tool(tool_call) messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) return 达到最大轮次限制任务未完成max_turns这个参数是保命的没有它模型可能无限循环。tool_choiceauto表示让模型自己决定要不要调工具你也可以设成强制调用某个工具。4.4 多轮对话与上下文管理生产环境里对话不是一次性的用户会连续提问。这时候上下文管理就很重要了。最简单的做法是把历史消息都存着但这样上下文会越来越长最后超出max-model-len。我的做法是保留最近 N 轮对话加上一个系统级的摘要。具体实现是维护一个消息列表超过阈值就把最早的消息压缩成一句摘要。这个摘要可以让模型自己生成也可以简单粗暴地截断。def trim_messages(messages, max_messages20): if len(messages) max_messages: return messages system messages[0] recent messages[-(max_messages - 1):] return [system] recent这个截断策略虽然简单但实测够用。更精细的做法是做语义摘要但那是另一个话题了。4.5 错误处理与重试机制模型调用不是 100% 可靠的网络会抖、服务会重启、模型偶尔会抽风。所以重试机制必须有。import time def call_with_retry(func, max_retries3): for i in range(max_retries): try: return func() except Exception as e: if i max_retries - 1: raise time.sleep(2 ** i)指数退避是个好习惯第一次等 1 秒第二次等 2 秒第三次等 4 秒。别用固定间隔容易把服务打崩。5. 生产级优化与常见问题排查5.1 性能优化让 Agent 跑得更快更稳Agent 的性能瓶颈通常在两个地方模型推理和工具执行。模型推理这块vLLM 已经帮你优化得差不多了你能做的是控制请求的并发数别一次性发太多把服务压垮。工具执行这块如果工具有网络请求一定要设超时否则一个慢工具会拖垮整个循环。还有个容易被忽略的点是流式输出。Agent 的最终答案如果等全部生成完再返回用户体感会很差。vLLM 支持流式你可以在最后一步用streamTrue把结果一点点吐给用户。5.2 常见问题速查表问题现象可能原因排查方向模型不调用工具工具描述不清 / tool_choice 设置问题检查工具描述尝试强制 tool_choice工具参数解析失败模型返回非法 JSON加 try-except返回错误让模型重试服务启动 OOM显存不足 / max-model-len 太大降量化、调小上下文、检查显存占用响应特别慢并发过高 / 工具阻塞限流、给工具加超时循环不终止缺少 max_turns强制加轮次上限上下文超长报错历史消息堆积做截断或摘要5.3 几个我踩过的坑第一个坑是工具描述里的歧义。我写过一个search工具描述是“搜索信息”结果模型什么鸡毛蒜皮的事都去调它。后来改成“搜索互联网上的实时信息当需要最新数据时使用”误用率立刻降下来了。第二个坑是参数类型不匹配。模型有时候会把数字传成字符串比如{count: 5}而不是{count: 5}。解决办法是在工具执行层做类型转换或者在参数描述里强调类型。第三个坑是vLLM 版本和模型不匹配。有次我换了个新模型怎么都加载不了折腾半天发现是 vLLM 版本太老不支持那个模型的架构。所以换模型前先查一下 vLLM 的版本兼容性。5.4 安全与边界控制Agent 能调用工具就意味着它能对真实世界产生影响。所以边界控制必须做。我的做法是给工具分级只读类工具查询、搜索随便调写入类工具发邮件、改数据要加确认机制。另外工具的参数要做校验别让模型传个奇怪的参数把你的数据库删了。注意永远不要给 Agent 无限制的系统权限。最小权限原则在 Agent 场景里同样适用。6. 从能跑到好用我的几点实战体会这套方案我从原型跑到生产前后迭代了十几个版本。最大的体会是Agent 的难点从来不在模型本身而在工程细节。模型选型、推理部署、工具设计、错误处理、上下文管理每一环都有坑每一环都需要打磨。Hermes 加 vLLM 这个组合在我看来是目前开源方案里性价比很高的一套。Hermes 的工具调用能力够用vLLM 的吞吐和稳定性经过验证Python 生态让工具开发几乎没有门槛。如果你正准备做 Agent 项目我建议就从这套开始先把主循环跑通再逐步加工具、加记忆、加优化。最后分享一个小技巧调试 Agent 的时候把每一轮的完整消息历史打印出来包括模型的原始响应。很多时候问题就藏在那些你没注意到的细节里比如模型返回了一个空 content 但带了 tool_calls或者工具返回的结果格式不对导致模型理解偏差。把这些看清楚大部分问题都能自己解决。