多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从模型选型到智能体落地:Hermes、Function Calling与vLLM生产级Agent工程实战

从模型选型到智能体落地:Hermes、Function Calling与vLLM生产级Agent工程实战 1. 从模型选型到智能体落地这套方案到底在解决什么问题过去大半年我一直在折腾 Agent 相关的项目从最开始的玩具级 Demo 到后来真正要扛线上流量的生产系统中间踩的坑实在太多了。很多朋友问我Hermes 这套东西到底怎么和 Agent 工程结合起来模型该怎么选Function Calling 怎么调才稳vLLM 部署要注意什么。说实话这些问题不是一两句话能说清的因为 Agent 工程本质上是一个系统工程模型只是其中一环编排、工具调用、状态管理、错误恢复每一块都能让你掉一层皮。我写这篇东西的目的很简单把我从零搭一套生产级 Agent 的完整路径摊开来讲包括模型选型时到底看哪些指标、Function Calling 的协议怎么设计才不容易崩、vLLM 部署大模型时那些官方文档不会告诉你的细节、以及 Python 侧怎么把这些东西串起来。适合谁看如果你已经写过几个 Agent Demo但一到生产环境就各种超时、幻觉、工具调用失败那这篇就是写给你的。如果你刚入门也没关系我会把基础概念用生活化的方式讲清楚保证你能跟上。核心关键词我先摆出来Hermes、Agent、Function Calling、Python、vLLM。这五个词基本覆盖了从底层推理到上层编排的全链路。Hermes 在这里我把它理解为一类具备强工具调用能力的模型系列Agent 是最终要交付的智能体形态Function Calling 是模型和外部世界交互的桥梁Python 是胶水语言vLLM 是推理加速引擎。把这五块拼起来你就能得到一个能跑在生产环境的智能体系统。2. 整体架构设计与模型选型思路拆解2.1 为什么 Agent 工程不能只盯着模型看很多人一上来就问“哪个模型最强”这其实是个伪命题。Agent 的表现取决于三个层面的配合模型本身的推理和工具调用能力、编排框架对状态的管控能力、以及推理服务的稳定性和延迟。我见过太多案例模型选了个榜单第一的结果 Function Calling 格式老是解析失败或者并发一上来推理服务直接 OOM整个 Agent 就废了。所以我的思路是先把架构定下来再倒推模型选型。一个典型的生产级 Agent 架构大概长这样最上层是业务逻辑中间是 Agent 编排层负责规划、记忆、工具路由下面是模型推理服务最底层是各种工具和外部 API。Hermes 这类模型的价值在于它对结构化输出和工具调用的原生支持比较好不需要你在 Prompt 里反复强调格式省了很多 token 也降低了出错率。2.2 模型选型的四个硬指标选模型的时候我一般看四个东西按优先级排Function Calling 的可靠性这是 Agent 的命根子。模型能不能稳定输出符合 JSON Schema 的调用参数直接决定了你的工具调用成功率。我实测下来Hermes 系列在这块的表现明显优于同尺寸的通用模型尤其是多工具并行调用的场景。上下文窗口和长文本衰减Agent 往往需要塞入大量历史对话和工具返回结果上下文窗口不够直接没法玩。但更关键的是长文本下的注意力衰减有些模型标称 128K实际到 32K 就开始胡言乱语了。推理延迟和吞吐生产环境不是单次调用你要考虑并发。一个 70B 的模型如果单次推理要 3 秒那 QPS 根本上不去。这时候 vLLM 的 PagedAttention 和连续批处理就派上用场了。部署成本显存占用、量化支持、是否容易做张量并行这些都要算进去。不是所有团队都有 8 卡 A100 的预算。2.3 vLLM 在架构中的定位vLLM 不是唯一选择但它是我目前用得最顺手的推理引擎。核心原因是它的PagedAttention机制把 KV Cache 的显存利用率拉高了一个档次配合连续批处理Continuous Batching在同样硬件下吞吐能比朴素实现高好几倍。对于 Agent 这种请求长度差异极大的场景vLLM 的调度器能动态合并请求避免长请求阻塞短请求。这里有个细节值得说vLLM 的调度逻辑是分层的它会把等待队列里的请求按 token 预算打包成一个 batch然后交给模型执行。如果你的 Agent 请求里有的很短比如简单的意图识别有的很长比如带大量工具返回的总结vLLM 能自动做优先级和打包优化这一点在自研推理服务里很难做好。3. Function Calling 协议设计与核心细节解析3.1 Function Calling 到底是怎么工作的很多人把 Function Calling 当成黑魔法其实原理很朴素。你在请求里附带一个工具列表每个工具用 JSON Schema 描述它的名字、参数和类型。模型在生成的时候如果判断需要调用工具就不会输出普通文本而是输出一段结构化的调用请求比如工具名加参数。你的代码解析这段结构执行真正的函数再把结果塞回对话历史让模型继续生成。关键在于“结构化输出”的稳定性。早期做法是在 Prompt 里写“请以 JSON 格式输出”然后正则提取这种做法在生产环境基本不可靠因为模型可能加个前缀、少个括号、或者把字符串引号搞错。Hermes 这类模型的好处是它在训练阶段就强化了工具调用的格式遵循配合 vLLM 的 guided decoding引导解码可以强制模型只能输出符合 Schema 的 token从根本上杜绝格式错误。3.2 工具 Schema 设计的五个坑我踩过的坑基本都集中在 Schema 设计上这里列几个最典型的参数类型过于复杂嵌套对象和数组虽然 Schema 支持但模型解析起来容易出错。我的经验是尽量扁平化超过两层的嵌套就拆成多个工具。枚举值不明确如果一个参数是枚举一定要把所有可能值列全并且加上描述。模型看不到枚举值就会瞎猜。必填项太多必填参数越多模型漏填的概率越高。能设默认值的就设默认值能推断的就别让模型填。工具描述太短工具描述是模型判断“什么时候该调用这个工具”的唯一依据。描述里要写清楚使用场景、不适用场景、以及和其他工具的区别。工具数量爆炸一次给模型几十个工具它的选择准确率会断崖式下降。我的做法是按场景分组每次只暴露当前场景相关的 5 到 8 个工具。3.3 用 vLLM 的 guided decoding 兜底vLLM 支持基于 JSON Schema 的引导解码这个功能在 Agent 场景下简直是救命稻草。你可以在请求里指定guided_json参数vLLM 会在解码时动态构建一个有限状态机只允许模型输出符合 Schema 的 token 序列。这样即使模型本身想“跑偏”也会被强行拉回来。配置的时候要注意guided decoding 会带来一定的解码开销因为每步都要做状态转移。对于工具调用这种短输出场景开销可以忽略但如果你用它来约束长文本生成延迟会明显上升。我的建议是只在工具调用节点开启普通对话节点关掉。4. 基于 vLLM 的推理服务部署实操4.1 环境准备与镜像选择部署 vLLM 最省事的方式是用官方 Docker 镜像。这里有个常见误区很多人以为镜像里带了模型其实官方镜像只带推理引擎模型要你自己挂载或者从模型仓库拉取。我一般会把模型权重提前下载到宿主机然后通过 volume 挂载进容器这样启动快也不用每次重新下载。镜像版本的选择要看你的模型和 CUDA 版本。太新的镜像可能和你的驱动不兼容太旧的又不支持新模型的架构。我的经验是选一个稳定的大版本比如 v0.27.x 系列然后在这个系列里挑最新的小版本。启动命令大概是这样docker run --gpus all \ -v /data/models:/models \ -p 8000:8000 \ --shm-size 16g \ vllm/vllm-openai:latest \ --model /models/your-model \ --served-model-name hermes-agent \ --max-model-len 32768 \ --gpu-memory-utilization 0.9 \ --enable-auto-tool-choice \ --tool-call-parser hermes几个参数值得展开说。--shm-size一定要给够vLLM 在多进程通信时对共享内存需求很大默认的 64M 根本不够会直接崩。--gpu-memory-utilization控制显存占用比例0.9 是个比较稳的值留一点给系统。--enable-auto-tool-choice和--tool-call-parser是开启工具调用支持的关键不同模型的解析器不一样Hermes 系列用hermes解析器。4.2 显存估算与并行策略部署前一定要算显存不然启动到一半 OOM 很浪费时间。粗略公式是模型参数量乘以精度字节数再加上 KV Cache。比如一个 7B 模型用 FP16权重占 14GBKV Cache 取决于并发数和上下文长度。如果上下文 32K、并发 16KV Cache 大概要 8 到 10GB总共 24GB 左右一张 3090 或 4090 勉强够。如果模型太大单卡放不下就要用张量并行。vLLM 支持--tensor-parallel-size参数把模型切到多张卡上。但要注意张量并行会带来卡间通信开销卡越多效率越低。我的经验是 2 卡并行的效率大概是单卡的 1.7 倍4 卡只有 2.8 倍左右所以能用单卡就别用多卡。4.3 服务健康检查与压测服务起来之后别急着接业务先做健康检查和压测。健康检查直接打/health接口返回 200 就说明服务活着。压测我一般用locust或者自己写个 Python 脚本模拟不同长度的请求并发打过去观察 P99 延迟和吞吐。这里有个坑vLLM 的默认调度策略是 FCFS先来先服务如果前面有个超长请求后面的短请求会被阻塞。可以通过调整--scheduler-policy参数改成优先级调度或者在上层做请求分流把长短请求打到不同的实例上。5. Python 侧 Agent 编排的完整实现5.1 对话循环与状态管理Agent 的核心是一个对话循环接收用户输入调用模型如果模型返回工具调用就执行工具把结果塞回历史再调用模型直到模型返回普通文本为止。这个循环看起来简单但状态管理很容易出问题。我用一个AgentState类来管理所有状态包括对话历史、工具调用记录、当前步骤数、以及各种中间变量。关键是要设置最大步数限制防止模型陷入死循环。我一般设 10 步超过就强制终止并返回当前结果。另外工具调用的结果要截断不能把整个 API 返回的几万字符全塞回去否则上下文瞬间爆掉。class AgentState: def __init__(self, max_steps10, max_tool_result_len2000): self.messages [] self.step 0 self.max_steps max_steps self.max_tool_result_len max_tool_result_len def add_tool_result(self, result): text str(result) if len(text) self.max_tool_result_len: text text[:self.max_tool_result_len] ...[truncated] self.messages.append({role: tool, content: text})5.2 工具注册与路由工具注册我推荐用装饰器模式这样新增工具只要写个函数加个装饰器就行不用改路由逻辑。每个工具要声明它的 Schema包括名字、描述、参数。路由的时候根据模型返回的工具名去注册表里查找不到就返回错误信息让模型重新决策。TOOL_REGISTRY {} def tool(name, description, parameters): def decorator(func): TOOL_REGISTRY[name] { function: func, schema: { type: function, function: { name: name, description: description, parameters: parameters, }, }, } return func return decorator tool( nameget_weather, description查询指定城市的当前天气适用于用户询问天气情况时, parameters{ type: object, properties: { city: {type: string, description: 城市名称如北京}, }, required: [city], }, ) def get_weather(city): return {city: city, temp: 25, condition: 晴}5.3 错误处理与重试机制生产环境里工具调用失败是常态网络超时、API 限流、参数错误都会发生。我的做法是给每个工具调用包一层重试最多重试两次指数退避。如果还是失败就把错误信息作为工具结果返回给模型让模型决定是换个工具还是直接告诉用户失败。这里有个细节错误信息要写得对模型友好。不要直接抛 Python 的 traceback那对模型来说是噪音。要写成“调用 get_weather 失败原因城市名称无法识别请检查后重试”这种结构化描述模型才能理解并做出正确决策。6. 常见问题与排查技巧实录6.1 工具调用格式解析失败这是最高频的问题。表现是模型返回的文本里工具调用格式不完整或者参数 JSON 解析报错。排查思路分三步先看是不是没开 guided decoding开了的话格式错误率应该极低再看 Schema 是不是太复杂简化后重试最后看模型本身是不是不支持工具调用换个模型验证。我遇到过一次很隐蔽的情况模型输出的 JSON 里有个不可见字符导致解析失败。后来在解析前加了一步清洗把所有非 ASCII 可见字符过滤掉才解决。这种问题官方文档根本不会提只能靠实际踩坑。6.2 推理服务 OOM 或响应超时OOM 一般是显存估算不足或者并发太高。先降--gpu-memory-utilization再降--max-num-seqs最大并发序列数。响应超时则要看是不是有长请求阻塞可以开启 chunked prefill让长请求分块处理不阻塞短请求。6.3 模型陷入循环或幻觉Agent 循环调用同一个工具、或者编造不存在的工具名都是常见问题。前者一般是工具返回结果没有提供足够信息模型觉得没解决问题就反复调用。解决方法是优化工具返回或者在 Prompt 里明确“如果工具返回结果已足够请直接回答”。后者则是工具列表和 Prompt 不一致检查一下注册的工具和传给模型的 Schema 是否同步。问题现象可能原因排查方向解决手段工具调用格式错误未开引导解码检查请求参数开启 guided_json参数解析失败Schema 过复杂简化嵌套结构扁平化参数服务 OOM显存不足查看显存占用降并发或量化响应超时长请求阻塞观察请求分布开启 chunked prefill模型循环调用工具结果不足检查返回内容优化返回或加提示编造工具名列表不同步对比注册表同步 Schema6.4 实操心得三条第一永远不要相信模型的输出格式哪怕它标称支持 Function Calling也要在代码侧做校验和兜底。第二工具描述要像写给新人看一样详细模型对描述的理解能力远不如人类模糊的描述会导致错误调用。第三压测要在上线前做我见过太多团队 Demo 跑得好好的一上生产就被并发打崩问题全出在推理服务的调度上。7. 生产级部署的扩展与优化方向7.1 多实例与负载均衡单实例 vLLM 总有性能上限生产环境一般要部署多个实例前面挂个负载均衡。但 Agent 场景有个特殊性同一个会话的请求最好打到同一个实例因为 KV Cache 是实例本地的跨实例会丢失缓存导致重复计算。解决方案是在负载均衡层做会话粘性根据会话 ID 哈希路由。7.2 量化与成本优化如果预算有限量化是最直接的降本手段。vLLM 支持 AWQ 和 GPTQ 量化4bit 量化能把显存占用降到 FP16 的四分之一左右精度损失在可接受范围内。但要注意量化后的模型工具调用能力可能会下降需要重新做一轮评测。我的经验是 7B 模型量化后影响不大70B 模型量化后复杂推理会明显变差。7.3 监控与可观测性生产系统没有监控就是裸奔。我一般会采集几个核心指标请求延迟P50/P99、工具调用成功率、模型输出 token 数、显存占用、以及每个工具的调用频次。这些指标能帮你快速定位问题比如工具调用成功率突然下降大概率是某个工具挂了或者 Schema 改了没同步。监控数据还可以反哺优化。比如你发现某个工具调用频次极高但成功率低那就要考虑是不是工具描述有问题或者这个工具本身设计得不合理。这种数据驱动的优化比拍脑袋改 Prompt 有效得多。7.4 记忆与上下文管理Agent 跑久了上下文会越来越长最终超出模型窗口。我的做法是分层记忆短期记忆保留最近几轮对话长期记忆把重要信息摘要后存到向量库需要时检索回来。摘要的时机很关键太早会丢信息太晚上下文已经爆了。我一般在上下文用到 70% 窗口时触发摘要把最早的几轮对话压缩成一段摘要。向量库的选择上轻量场景用 FAISS 就够了要持久化和分布式就上 Milvus 或 Qdrant。嵌入模型可以用 vLLM 一起部署这样推理和嵌入共享 GPU省资源。不过要注意显存分配嵌入模型虽然小但也会占一部分。这套东西我从零搭到现在稳定运行前后迭代了十几个版本最大的体会是Agent 工程的难点从来不在模型本身而在模型之外的系统工程。模型选型、推理部署、协议设计、错误处理、监控运维每一环都能决定最终成败。把每一环都做扎实Agent 才能真正从 Demo 走向生产。
返回列表