用 JEV 决策模型接管 Agent 路由判断(二):工程实现与 4 个坑

设计层决定「做什么」,实现层决定「坑在哪」。

上一篇把设计层定了:闭集 + 概率、两段式采信、默认关闭 + 影子先行。本篇讲怎么把它写进代码,以及跟真实决策模型对接时撞上的四个坑------这四个坑都不在文档里,只能实测出来,而它们直接决定了阈值、费用口径和审计字段该长什么样。

先对齐一个数量级:判定一次外呼只有几百毫秒 ,而它省下的是一整轮秒级推理(这条账的完整算法在第四篇)。这笔账只有在判定靠谱时才成立,所以下面这四个坑值得一个一个抠。

一、一个端口 + 一个实现:上层不感知 Jev

判断这一步被切成「端口 + 实现」:

arduino 复制代码
org.dromara.ai.core.route
├── RouteJudge                // 端口:judge(请求) → 判定
├── JevRouteJudge             // 实现:JevClient(协议原生,靠配置切厂商)
└── RouteJudgeRequest/Verdict // 入参:state + 候选;出参:choice + 概率 + 耗时 + token

上层 RouteJudgeService 只注入端口 ,不感知 Jev 的任何细节;门面注入这个 service,而编排链路不参与判断------判断永远是「额外一步」,不搅进编排。

这么切有三个直接好处:单测可以打桩(不需要真的外呼)、换模型换厂商不动上层、判定逻辑能独立演进而不碰编排。

实现类内部包掉四件事:构造请求、调用、校验、超时与异常归一化------异常一律转成「无判定」,绝不往上抛(第六节讲为什么)。

二、为什么不用框架自带的三个 JEV 中间件

AgentScope 2.0.4 的 agentscope-extensions-jev 模块只提供三个参考中间件,覆盖的是另外三类判断:

中间件 拦截点 判断对象
JevModelRouterMiddleware onAgent / onModelCall 选模型
JevToolSelectionMiddleware onReasoning 选工具
JevAutoModeMiddleware onActing 工具执行的安全性

没有「选专家」的中间件 ;而且我们的路由判断发生在编排层 ,不在小Z 的 Agent 回调里------中间件根本挂不上。所以这里不是重复造轮子,而是直接注入 JevClient、自建判断组件。

顺带两条版本事实(都实测过):在线文档里出现的 JevExecution.Mode.SHADOW/ENFORCE 执行模式、以及 team routing / phase routing,在 2.0.4 中不存在 (本地全仓 grep 零命中)。所以「影子 / 接管」两种执行模式是我们自建的------这也解释了第一篇那套 shadow → gate 为什么是自己写的,而不是框架开关。

三、输入怎么拼:只放判断必需的事实

state 是决策模型的全部输入。原则是只放判断必需的东西,不喂完整对话历史------既是判断质量的要求(它不做长文理解),也是数据出境面的控制。

  • 本轮用户原话:不拼历史全文;
  • 候选清单 :agent_code + 名称 + 描述(描述即「擅长」),与平台既有的候选清单同源,含可见范围过滤;
  • 会话槽位摘要:语言 / 时区 / 任务线,可为空,不求全。

两个实现细节值得单拎出来说:

  1. 候选清单永不截断。 state 有长度上限(协议侧 65536 token,实现侧按字符数做前置保护),但曾经的截断是从头截------长原话会把候选清单整段切掉,等于把「选项」删了再让模型选。这个既有隐患必须一并修掉。
  2. 选项 key 用 agent_code,不用主键。 与下游 expert_agent_key 同口径,避免「判定回了一个雪花 ID、下游认不出来」。另外固定补两个选项:self(自答)与 other(无法判断归属)------other 是逃生口,不是错误码。

四、四个真实的坑

1. 概率和不保证为 1,而框架会因此直接抛异常

百炼返回的概率是两位小数 ,和可能是 0.99 / 1.01(稳定复现:{self:0, a:0.44, b:0.52, other:0.03} → 0.99 )。而框架 JevClient 的校验容差只有 1e-6 × 选项数,会直接抛 JevException;更要命的是这个异常不带响应体 (getResponseBody() 为 null),「捕获异常、自己兜底解析」这条路是被堵死的。

处置:复用协议 DTO,自建调用与宽松校验------容忍 ≤0.05 的概率和偏差并告警留痕,而不是丢掉一次判定。

2. 是非题(noul)不返回置信度

只有 choice / score 带 confidence。所以将来若要用「是否需要人工确认」这类是非题,不能依赖置信度阈值 ,只能自己按 P(yes) 定口径。

3. usage.outputTokens 恒为 0

百炼只返回 input_tokens;DTO 的 outputTokens 是基本类型,缺字段被填 0 而不是报错。费用口径只能按输入 token 计 ------拿 outputTokens 参与计算,结果一定是 0。

4. 判定有波动

同一输入两次调用,top 选项一致,但概率 0.93 → 0.86、置信度 0.90 → 0.81。结论很直接:阈值不能拍脑袋,必须用自有样本校准(第三篇讲怎么校准)。

五、每一次判定都要留痕

判断这一步最大的额外收益不是延迟,而是它变成了可分析的数据。所以每一次判定(含失败)都留痕:来源、选择、置信度、概率分布、耗时、错误码、执行模式。

错误码要能被人读懂,并且不静默:

  • margin_rejected:top1 达阈值但 margin 不足------这正对应「勉强第一」型误接管的观察面;
  • circuit_breaker_open:熔断期间未判定;
  • 超时 / 非 2xx / 越界各有对应码。

前端把这些码翻成中文文案、并落到「结构化决策判定」详情卡里;未知码原样透出------不认识的码也要看得见,它往往是新问题的第一个信号。

六、实现层的两条降级纪律

  1. 异常归一化为「无判定」:超时、非 2xx、解析失败、选项越界,全部转成「本轮没有判定」,由上层回落原有工具决策。绝不抛异常、绝不重试专家、绝不换人。
  2. 取数失败必须局部降级:比如拼会话上下文时查上一轮结果失败,应当是「不拼这个分区、继续判定」,而不是「整轮不判定」------一次 DB 抖动不该让判定能力整体失效。

七、小结

对接一个「协议同源」的模型,最贵的不是写调用代码,而是把它的怪癖当成事实来适配 :概率和不为 1、是非题没有置信度、缺字段被静默填 0、同一输入会波动。这四条决定了你的阈值、费用口径和审计字段------先实测,再定参数。

下一篇讲怎么把这份能力放上去并且收得住:影子数据怎么变成阈值、超时与熔断怎么兜底、追问场景怎么做到根本不外呼,以及数据出境与权限边界这两条合规红线。

项目已在 Gitee 开源:gitee.com/zl3624/biz-...。欢迎来 Issue 交流。

相关推荐
YangYang9YangYan1 小时前
2027秋招:统计学专业如何把数据能力用在商品计划、会员运营与市场研究
大数据·人工智能
东离与糖宝1 小时前
不用重写Agent,rpi长连接快速搭建飞书智能体机器人
人工智能
u1301301 小时前
GitHub 热榜项目:日榜(2026-10-16)
人工智能·github
老唐7771 小时前
OpenCV5实测:纯CPU跑YOLO26目标检测
图像处理·人工智能·opencv·yolo·目标检测·计算机视觉
happyprince1 小时前
02-具体观 · JevAny算法与实现剖析
人工智能·算法
是店小二呀1 小时前
想用自己的音色给视频配音?我用Index-TTS跑通参考音频、语音生成和远程访问
人工智能·音视频·语音识别
时间的情敌1 小时前
AI 对前端开发的挑战与机遇
人工智能
揽秀亭长1 小时前
提取视频文案的工程选型|五类转写方案对比、可复现脚本与报错排查表
人工智能·音视频