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 python=3.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_url="http://localhost:8000/v1", api_key="EMPTY") def run_agent(user_input, max_turns=10): messages = [ {"role": "system", "content": "你是一个智能助手,可以调用工具来完成任务。"}, {"role": "user", "content": user_input} ] for turn in range(max_turns): response = client.chat.completions.create( model="hermes", messages=messages, tools=tools, tool_choice="auto" ) 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_choice="auto"表示让模型自己决定要不要调工具,你也可以设成强制调用某个工具。
4.4 多轮对话与上下文管理
生产环境里,对话不是一次性的,用户会连续提问。这时候上下文管理就很重要了。最简单的做法是把历史消息都存着,但这样上下文会越来越长,最后超出max-model-len。
我的做法是保留最近 N 轮对话,加上一个系统级的摘要。具体实现是维护一个消息列表,超过阈值就把最早的消息压缩成一句摘要。这个摘要可以让模型自己生成,也可以简单粗暴地截断。
def trim_messages(messages, max_messages=20): 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_retries=3): 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 支持流式,你可以在最后一步用stream=True把结果一点点吐给用户。
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,或者工具返回的结果格式不对导致模型理解偏差。把这些看清楚,大部分问题都能自己解决。