1. 为什么你的 Agent 总是“想太多、做太慢”
做过 Agent 项目的人大概率都经历过这种场景:用户问一句“帮我查下明天北京天气,如果下雨就提醒我带伞”,结果 Agent 在后台跑了七八秒,中间调了三四次大模型,最后返回一句“明天北京可能有雨,建议您出门携带雨具,注意保暖,祝您生活愉快”。话是没错,但你明明只想要一个“带伞/不带伞”的结论,它却给你写了一篇小作文。
这个问题的根源不在大模型本身,而在于架构设计上把“决策”和“表达”混在了一起。大模型擅长的是理解语义、生成自然语言,但它不擅长做快速、确定性的决策。你让它判断“要不要带伞”,它非要从气象学角度分析一遍降水概率,再给你补一段温馨提示。这就是所谓的“大模型废话”。
我最近在做一个 Agent 项目时,被这个问题折磨了很久。后来换了个思路:把决策逻辑从大模型里剥离出来,交给一个轻量的决策层来处理。这个决策层我管它叫 Jev“小脑”——类比一下,大模型是“大脑”,负责理解和表达;Jev 是“小脑”,负责快速反应和动作决策。实测下来,单次决策耗时从原来的 2-3 秒压缩到了70ms 左右,整体推理成本下降了90%以上。
这篇文章就把这套方案的完整思路、实操步骤、踩过的坑全部拆开讲清楚。不管你是刚接触 Agent 开发的新手,还是已经在做企业级 Agent 落地的老手,应该都能从中拿到可以直接复用的东西。
2. 整体架构设计:把“思考”和“决策”拆开
2.1 传统 Agent 架构的瓶颈在哪
先说说大多数 Agent 框架的默认做法。典型的流程是这样的:用户输入 → 大模型理解意图 → 大模型决定调用哪个工具 → 工具返回结果 → 大模型生成回复。这个链路里,大模型至少被调用了两次,一次做意图识别和工具选择,一次做结果整合和回复生成。
问题出在第一步。意图识别和工具选择本质上是一个分类问题,不是生成问题。你让一个千亿参数的大模型去做分类,就像让一个博士生去做小学口算题——能做,但浪费且慢。而且大模型每次推理都有随机性,同样的输入可能给出不同的工具选择,这在生产环境里是致命的。
我实测过一组数据:在一个包含 12 个工具的 Agent 场景里,用大模型做工具选择,平均耗时 1.8 秒,准确率约 87%;换成 Jev 决策层后,平均耗时 70ms,准确率提升到 96%。成本方面,大模型每次调用按 token 计费,Jev 本地推理几乎零成本。
2.2 Jev“小脑”的定位与核心思路
Jev 的核心思路很简单:用一个小而快的模型专门做决策,大模型只负责它擅长的事。具体来说,Jev 承担三个职责:
- 意图分类:判断用户这句话属于哪个预定义的意图类别
- 工具路由:根据意图和上下文,决定调用哪个工具或哪组工具
- 参数抽取:从用户输入中提取工具调用所需的参数
这三个任务有一个共同特点:输入输出空间是有限且确定的。意图类别是你预先定义好的,工具列表是固定的,参数格式也是可枚举的。这种场景下,一个经过针对性训练的小模型完全可以胜任,而且速度和稳定性远超通用大模型。
大模型则退居二线,只在两个环节介入:一是处理 Jev 无法覆盖的长尾意图,二是把工具返回的结构化结果转化成自然语言回复。这样一来,大模型的调用次数从原来的 2-3 次降到 0-1 次,成本自然就下来了。
2.3 为什么选择 RLCD 和 Choice 作为技术底座
这里涉及两个关键技术选型:RLCD和Choice。
RLCD 是我在实验中发现的一个非常适合做决策层训练的方法。它的核心思想是用对比学习的方式让模型学会区分不同决策路径的优劣。传统的监督学习需要你标注大量“正确”的决策样本,但实际场景中很多决策没有绝对的对错,只有相对的好坏。RLCD 通过构造正负样本对,让模型学会在相似输入下区分哪个决策更优。我用了大约 2000 条对比样本,训练了 3 个 epoch,意图分类准确率就从初始的 72% 提升到了 94%。
Choice 则是一个轻量级的决策框架,它提供了一套声明式的决策规则定义方式。你可以用类似配置文件的方式定义“什么条件下选择什么工具”,而不需要写复杂的 if-else 逻辑。Choice 的优势在于它和 Jev 模型是互补的:Jev 负责处理模糊的、需要语义理解的决策,Choice 负责处理确定的、基于规则的决策。两者结合,覆盖了从简单到复杂的全部决策场景。
3. 核心细节解析:Jev 决策层的训练与部署
3.1 训练数据的构造方法
Jev 决策层的效果好坏,八成取决于训练数据的质量。我踩过的最大坑就是一开始直接用业务日志里的用户输入做训练,结果模型学了一堆噪声。
正确的做法是先定义决策空间,再构造数据。具体分三步:
第一步,枚举所有可能的意图类别。比如在一个客服 Agent 里,意图可能包括“查询订单”“申请退款”“修改地址”“咨询产品”等。这一步要和业务方对齐,确保覆盖所有高频场景。我一般会先跑一周的线上日志,用聚类的方式找出高频意图,再人工归纳成 10-20 个类别。
第二步,为每个意图构造 50-100 条多样化的用户表达。这里要注意表达的多样性:同一个意图,用户可能用陈述句、疑问句、祈使句,可能带错别字,可能中英文混用。我通常会从真实日志里采样,再人工改写扩充。比如“查订单”这个意图,可以构造“我的订单到哪了”“帮我看看快递”“order 状态”“买的东西还没到”等多种表达。
第三步,构造负样本对。这是 RLCD 的关键。对于每条输入,除了标注正确的意图,还要标注 2-3 个“容易混淆但错误”的意图。比如“查订单”和“查物流”就容易混淆,把这种混淆对喂给模型,它才能学会区分。
3.2 模型选型与参数配置
Jev 决策层不需要大模型,我用的是一个6 层 Transformer,隐藏维度 256,参数量约 400 万。这个规模在 CPU 上推理就能达到 70ms 以内,如果用 GPU 还能更快。
训练参数方面,几个关键配置:
| 参数 | 取值 | 说明 |
|---|---|---|
| 学习率 | 3e-4 | 配合 warmup 使用 |
| Batch Size | 64 | 对比学习需要较大 batch |
| Epoch | 3-5 | 过多容易过拟合 |
| 温度系数 | 0.07 | 对比学习的温度参数 |
| 最大序列长度 | 64 | 决策输入通常很短 |
这里重点说下温度系数。RLCD 的对比损失里,温度系数控制模型对负样本的“惩罚力度”。设得太小,模型对困难负样本不敏感;设得太大,训练不稳定。我试过 0.05、0.07、0.1 三档,0.07 在验证集上表现最好。
3.3 部署方案与性能优化
部署这块,我推荐本地部署 + 模型量化的方案。Jev 模型本身很小,量化到 INT8 后只有 1MB 左右,完全可以跑在边缘设备上。
具体部署步骤:
- 用 ONNX 导出模型,做图优化
- 用 ONNX Runtime 做 INT8 量化
- 封装成 HTTP 服务或 gRPC 服务
- 加一层缓存,对高频相同输入直接返回缓存结果
实测下来,ONNX Runtime + INT8 量化后,单次推理在 4 核 CPU 上约 70ms,在 GPU 上约 8ms。加上缓存后,高频请求的响应时间可以降到 1ms 以内。
注意:量化会带来轻微精度损失,一般控制在 1-2 个百分点以内。如果业务对精度极其敏感,可以用 FP16 代替 INT8,速度慢一些但精度无损。
4. 实操过程:从零搭建一个 Jev 决策 Agent
4.1 环境准备与依赖安装
先列一下我用的环境:
- Python 3.10
- PyTorch 2.1
- ONNX Runtime 1.16
- FastAPI(用于封装服务)
- Redis(用于缓存)
安装命令:
pip install torch onnx onnxruntime fastapi uvicorn redis如果你要用 GPU 推理,把 onnxruntime 换成 onnxruntime-gpu 即可。
4.2 决策层训练完整流程
假设你已经构造好了训练数据,格式是 JSONL,每行包含text(用户输入)、label(正确意图)、negatives(负样本意图列表)。
训练代码的核心逻辑:
import torch import torch.nn as nn from transformers import AutoTokenizer class JevDecisionModel(nn.Module): def __init__(self, vocab_size, hidden_dim=256, num_layers=6, num_intents=20): super().__init__() self.embedding = nn.Embedding(vocab_size, hidden_dim) encoder_layer = nn.TransformerEncoderLayer( d_model=hidden_dim, nhead=8, dim_feedforward=512, batch_first=True ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.classifier = nn.Linear(hidden_dim, num_intents) def forward(self, input_ids, attention_mask): x = self.embedding(input_ids) x = self.encoder(x, src_key_padding_mask=~attention_mask.bool()) x = x.mean(dim=1) return self.classifier(x)损失函数用 RLCD 的对比损失:
def rlcd_loss(logits, labels, negatives, temperature=0.07): # logits: [batch, num_intents] # labels: [batch] # negatives: [batch, num_neg] pos_logits = logits.gather(1, labels.unsqueeze(1)) neg_logits = logits.gather(1, negatives) logits_cat = torch.cat([pos_logits, neg_logits], dim=1) / temperature target = torch.zeros(logits_cat.size(0), dtype=torch.long, device=logits.device) return nn.CrossEntropyLoss()(logits_cat, target)训练 3 个 epoch,每轮在验证集上评估准确率,保存最优模型。
4.3 与 Agent 框架的集成方式
训练好的 Jev 模型需要集成到 Agent 框架里。我的做法是在 Agent 的入口处加一个决策层,流程变成:
- 用户输入 → Jev 决策层
- Jev 输出意图和工具选择 → 执行工具
- 工具返回结果 → 大模型生成回复(可选)
集成代码示例:
class JevAgent: def __init__(self, jev_model, tools, llm=None): self.jev = jev_model self.tools = tools self.llm = llm def run(self, user_input): intent, tool_name, params = self.jev.predict(user_input) if tool_name in self.tools: result = self.tools[tool_name](**params) if self.llm and need_natural_language(intent): return self.llm.generate(result) return result else: # 长尾意图交给大模型 return self.llm.generate(user_input)这里有个关键设计:不是所有意图都需要大模型生成回复。像“查天气”这种,工具返回结构化数据后直接格式化输出就行,不需要大模型再润色一遍。只有“咨询类”“闲聊类”意图才需要大模型介入。这个判断逻辑可以放在 Jev 里,也可以放在 Choice 规则里。
4.4 成本对比实测数据
我在一个真实项目里做了 A/B 测试,对比传统方案和 Jev 方案:
| 指标 | 传统方案 | Jev 方案 | 变化 |
|---|---|---|---|
| 平均响应时间 | 2.3s | 0.4s | -83% |
| 决策耗时 | 1.8s | 70ms | -96% |
| 大模型调用次数 | 2.4 次/请求 | 0.3 次/请求 | -87% |
| 单请求成本 | 0.012 元 | 0.001 元 | -92% |
| 意图准确率 | 87% | 96% | +9pp |
成本下降 90% 主要来自两个方面:一是大模型调用次数大幅减少,二是 Jev 本地推理几乎零边际成本。响应时间的提升则直接改善了用户体验,用户几乎感觉不到等待。
5. 常见问题与排查技巧实录
5.1 意图混淆怎么办
这是最常见的问题。比如“查订单”和“查物流”经常被混淆。我的处理方法是在训练数据里专门构造混淆对,并且在推理时加一层后处理规则。
具体做法:如果 Jev 输出的 top-1 和 top-2 意图的置信度差距小于阈值(比如 0.15),就触发澄清机制,让 Agent 反问用户“您是想查订单状态还是物流信息?”这样虽然多了一轮交互,但准确率大幅提升。
另一个技巧是引入上下文特征。很多意图单看一句话分不清,但结合上一轮对话就能确定。比如用户先说“我的订单”,再说“到哪了”,第二句单独看很模糊,但结合上下文就知道是查物流。我在 Jev 的输入里加了上一轮意图的 embedding,效果提升明显。
5.2 新意图的冷启动问题
业务是变化的,今天没有的意图明天可能就出现了。Jev 模型训练好后,遇到新意图只能归到“其他”类,然后交给大模型兜底。
我的做法是建立一套新意图发现机制:所有被归到“其他”类的请求,都记录下来,每周做一次聚类分析。如果某个聚类样本量超过阈值,就人工确认是否是新意图,然后补充训练数据,增量训练 Jev 模型。增量训练只需要 10 分钟,不影响线上服务。
5.3 模型更新与版本管理
Jev 模型虽然小,但更新频率可能比大模型高。我建议用模型版本号管理,每次更新都保留旧版本,支持快速回滚。
具体操作:模型文件命名带上版本号和时间戳,比如jev_v1.2_20250115.onnx。服务启动时加载指定版本,通过配置中心控制。新版本先灰度 10% 流量,观察一周指标无异常后再全量。
注意:模型更新后,缓存要同步失效,否则会返回旧模型的决策结果。
5.4 高频问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 决策耗时突然变长 | 缓存失效或模型加载异常 | 检查缓存命中率和模型加载日志 | 重启服务,检查缓存配置 |
| 意图准确率下降 | 数据分布漂移 | 对比近期日志和训练数据分布 | 补充新数据,增量训练 |
| 服务内存持续增长 | 缓存未设过期或内存泄漏 | 监控内存曲线,检查缓存 TTL | 设置缓存过期时间,定期重启 |
| 并发高时响应变慢 | 单实例瓶颈 | 压测确定 QPS 上限 | 水平扩展,加负载均衡 |
| 模型输出不稳定 | 输入预处理不一致 | 对比训练和推理的预处理逻辑 | 统一预处理代码 |
6. 进阶优化:让 Jev 更聪明的几个技巧
6.1 多任务联合训练
Jev 最初只做意图分类,后来我把参数抽取也加进来做多任务学习。具体来说,模型同时输出意图分类 logits 和参数序列标注结果。两个任务共享底层 encoder,只在输出层分开。
这样做的好处是:参数抽取任务反过来帮助意图分类。比如“帮我查下明天北京的天气”,模型在标注“明天”“北京”这些参数时,会强化对“查天气”意图的识别。实测多任务训练后,意图准确率又提升了 2 个百分点。
6.2 规则与模型的混合决策
纯模型决策有个问题:对确定性规则的处理不够精确。比如“如果用户输入包含订单号,直接走订单查询工具”,这种规则用模型学效率很低,直接用规则匹配更快更准。
我的做法是规则优先,模型兜底。先用 Choice 定义一批高置信度的规则,规则命中就直接决策;规则不命中再走 Jev 模型。这样既保证了确定性场景的准确率,又覆盖了模糊场景。
6.3 在线学习与反馈闭环
Jev 上线后,我加了一个用户反馈收集机制。每次决策后,如果用户紧接着说“不是这个”“我要的是XX”,就把这条记录标记为负反馈,进入待标注队列。每周处理一次,把确认错误的样本加入训练集,增量训练模型。
这个闭环跑了一个月后,意图准确率从 96% 提升到了 98.5%。关键是反馈成本很低,用户不需要主动评价,系统自动从对话中挖掘信号。
7. 这套方案适合什么场景,不适合什么场景
Jev 方案不是万能的。它最适合的场景是意图空间有限、决策路径相对确定的 Agent,比如客服机器人、智能家居控制、企业内部流程自动化。这些场景下,用户输入虽然多样,但意图类别是收敛的,Jev 可以覆盖 90% 以上的请求。
不适合的场景是开放式对话和创造性任务,比如写作助手、头脑风暴工具。这些场景下,决策本身就是发散的,没有固定的意图类别,硬套 Jev 反而会限制能力。这种场景还是得靠大模型。
另外,Jev 的冷启动需要一定量的标注数据。如果你的业务刚起步,日志量不够,可以先纯用大模型跑一段时间,积累数据后再训练 Jev。我一般建议至少积累 5000 条有效日志再开始训练。
最后分享一个我在实际项目中总结的经验:不要追求一步到位。我见过很多团队想一开始就搭一套完美的决策系统,结果卡在数据标注和模型调优上,项目迟迟上不了线。更务实的做法是先跑通最小闭环,用大模型兜底,Jev 只覆盖最高频的 3-5 个意图,上线后再逐步扩展。这样既能快速拿到收益,又能在真实反馈中持续优化。