我自己搞 Agent 项目有一阵子了,最头疼的从来不是主模型生成得不够好,而是它“判断”得不够稳。同一个用户输入,上午判断该调天气工具,下午就当成闲聊处理了;让它输出一个结构化参数,它非要在 JSON 前面加一句“好的,我来帮你查询”。后来我把“判断”这件事从主模型里拆出来,单独做了一层判断器,先后试了 Laya 和 Jev 两条路,从模型获取到本地部署再到接进 Agent 链路,坑踩了不少,但也把一套可行的方案跑通了。这篇就当作一次复盘,聊聊判断器到底解决什么问题、Laya 和 Jev 怎么部署,以及最后到底怎么选。
1. 为什么 Agent 需要独立的“判断器”
1.1 主模型负责“生成”,判断器负责“定夺”
Agent 本质上是“感知-决策-行动”的循环:拿到用户输入,决定下一步干什么,调用工具,拿到结果,再决定要不要继续。大部分团队一开始都会让主模型直接做这个决策,也就是在系统提示词里写清楚“如果用户要求查天气,就调用天气工具,参数格式如下”,然后靠 few-shot 示例来稳定输出。
这套做法在小 Demo 里没问题,但一旦到了真实生产环境,主模型“生成能力强、决策稳定性差”的毛病就暴露出来了。举个例子:用户说“帮我看看北京明天冷不冷”,主模型可能在一次调用里判断对了,生成{"city": "北京", "date": "2025-06-15"},但下一次同样的话,它可能就理解成“用户想知道北京的天气趋势”,直接不调工具、用知识库里的百科内容硬答。这不是模型笨,而是决策和生成本来就是两码事。
我拿生活里的场景做个类比:主模型像一个全能型员工,写文案、写代码、理解语义都不错,但你让他一个人在流水线上又干活又当质检员,他一定会顾此失彼。判断器的价值,就是在流水线上单独安排一个“班组长”,只负责拍板,不负责具体执行。
还有一个隐蔽的问题:工具参数。主模型生成参数时,字段名、格式经常漂移。今天生成{"city": "北京"},明天可能生成{"location": "Beijing, CN"},后天可能把日期格式写错。工具端如果做了严格校验,这些请求会直接被拒绝;如果没做校验,脏数据就进业务系统了。判断器就是要在这个位置把不确定性按下去。
1.2 判断器要满足的三个硬指标
我对判断器的要求,其实就三条。
第一,输出必须能被程序直接解析。判断器返回的结果不是给人看的,是给代码用的。它应该说{"intent": "tool"},而不是“我觉得用户可能需要调用天气工具”。这两者对用户体验的差别是:前者能稳定驱动后续流程,后者只能让解析代码写一堆正则去猜。
第二,延迟和开销要可控。Agent 主链路一次完整响应可能就要 2 到 5 秒,判断器如果每次判断都花 1 秒以上,整个体验就毁了。所以判断器要轻,最好控制在几百毫秒以内。这个指标直接决定了模型选型:大的通用模型往往效果不错,但延迟和成本都高;小而专的模型反而更合适。
第三,可以独立迭代、独立部署。判断器和主模型之间应该是解耦的。主模型升级了,判断器不用跟着动;判断器判断不准,也不至于影响主模型的生成能力。更重要的是,两者可以分别扩容。Agent 系统扛并发时,通常是主模型先成为瓶颈,但如果判断器设计得不好,它反而会先躺平。
我见过不少团队把“判断”完全塞进主模型的提示词里,然后发现主模型升级一次,判断逻辑就崩一次。把判断器独立出来之后,调整判断策略只需改判断器一侧的配置和样本,主链路完全不动,这一层的收益长期来看非常大。
1.3 Laya 和 Jev 的分工:一个管“要不要”,一个管“对不对”
在我实际的项目里,Laya 和 Jev 是两个不同方向上的判断器,解决的问题不一样。
Laya 我理解成“轻量路由判断模型”,核心任务是快速回答“这个输入要不要动用工具”。它跑在本地,用量化小模型部署,延迟低、费用低,适合做链路里的第一道闸门。每次用户请求进来,先让 Laya 给一个意图分类,比如chat、tool、refuse三选一,然后再决定走哪条分支。
Jev 则更像“结构化约束判断器”,核心任务是回答“这个参数对不对、这个输出合不合规”。它擅长生成严格符合 JSON Schema 的内容,能做参数整形、结果校验、格式修复。它在链路中通常出现在更靠后的位置:已经决定要调工具了,但工具参数需要精确;或者工具返回了结果,但结果不符合下游要求,需要二次加工。
| 维度 | Laya | Jev |
|---|---|---|
| 定位 | 轻量路由 / 意图判断 | 结构化输出与校验 |
| 部署形态 | 本地推理(Ollama/vLLM) | API 或本地推理 |
| 典型延迟 | 百毫秒级 | 百毫秒到秒级 |
| 适用环节 | 意图分类、是否调用工具 | 参数生成、输出校验、格式修复 |
| 算力要求 | 7B 量化模型即可 | 视模式而定,通常需要更强的约束能力 |
需要说明的是,Laya 和 Jev 是我在具体 Agent 链路里用到的两个判断器方向,每个人手里的版本可能不一样,但部署和选型的思路是可以通用的。下面我把两条路分别拆开讲。
2. 部署 Laya:给 Agent 装一个“轻量路由大脑”
2.1 环境准备与模型获取:先小后大,跑通再换
Laya 这类轻量模型的部署方式,和普通开源模型没有本质区别。我建议的环境是:一台 16GB 内存的机器起步,有 NVIDIA 显卡更好,没有显卡的话纯 CPU 也能跑,只是响应速度会慢一些。如果你的目标平台是 Jetson Orin、RK3588 这类边缘设备,也可以跑小尺寸量化模型,但上下文长度和并发数要适当压低。
模型获取有两条路。最简单的是用 Ollama 直接拉取,它会自动处理量化、依赖和运行时。命令就一行:
ollama pull laya:7b-q4_K_M ollama serve拉取之后,默认监听本地的11434端口,可以直接用 HTTP 调用,不用自己写推理代码。
如果并发要求比较高,我建议直接用 vLLM。vLLM 自带了 continuous batching(连续批处理),GPU 利用率比 Ollama 高不少,适合做服务化部署。启动命令类似这样:
vllm serve laya-7b --quantization awq --max-model-len 8192 --max-num-seqs 32这里面几个参数值得解释一下。--quantization awq表示用 AWQ 量化,加载后显存占用比原始 FP16 低很多;--max-model-len 8192限制了上下文长度,判断器通常不需要太长上下文,限制它能省显存;--max-num-seqs 32是并发序列上限,vLLM 会在内部做调度,不会因为请求太多直接 OOM 崩溃。
一个经验是:先拿 7B q4 量化跑通全链路,再根据准确率和延迟决定要不要换更大模型。很多人一上来就想部署最大的模型,结果显存不够、延迟爆炸,链路根本跑不起来。判断器这个位置,最忌讳一步到位。
2.2 一条可直接抄的意图路由代码
模型部署好之后,下一步就是让 Agent 的主链路调用它。我用得最多的场景是意图路由:用户输入一句话,判断器返回chat、tool、refuse三个分类之一。
chat:直接对话,不需要调用任何工具;tool:需要调用工具,进入工具选择与参数生成流程;refuse:输入内容不在处理范围内,直接拒绝。
下面这段代码是我在生产环境里用过的简化版,直接贴给你参考:
import json import requests LAYER_URL = "http://127.0.0.1:11434/api/generate" def route(text: str) -> str: prompt = ( "你是 Agent 的意图判断器。" "只能输出 JSON:{\"intent\": \"chat\" | \"tool\" | \"refuse\"}。" "不要输出任何其他文字。\n" f"用户输入:{text}" ) resp = requests.post( LAYER_URL, json={ "model": "laya:7b-q4_K_M", "prompt": prompt, "stream": False, "temperature": 0, "top_p": 1, "format": "json", }, timeout=10, ) data = resp.json()["response"] return json.loads(data).get("intent", "chat")每个参数都有讲究。temperature=0是为了让判断稳定,判断器不是一个创作工具,不需要随机性;top_p=1是去掉 nucleus sampling 的截断,让输出更确定;format="json"是 Ollama 的 JSON 模式开关,如果用的推理服务不支持这个参数,就必须在 prompt 里反复强调“只能输出 JSON”,并且加上格式示例。
timeout=10也不能省。判断器只是链路的一部分,它挂了不能让整个 Agent 跟着挂。超时之后按chat兜底,至少用户还能正常对话,只是工具调用能力暂时降级。这个兜底逻辑在真实环境中非常关键。
2.3 并发扛不住怎么办:从单实例到多实例
模型部署中最容易被低估的问题是并发。很多人把模型启动起来,单个请求测一下,200 毫秒返回,觉得稳了,结果线上同时进来 20 个用户请求,判断器直接排起长队,等待时间从 200 毫秒变成 5 秒,主链路全被拖死。
我自己在并发压力下踩过这个坑,总结下来有四招。
第一招:能上 vLLM 就上 vLLM。Ollama 适合本地开发调试,但生产环境并发要求高,vLLM 的 continuous batching 能显著提升吞吐。实测下来,在 24G 显存的卡上跑 7B 量化模型,vLLM 单实例能扛住几十路的并发请求,而 Ollama 默认配置下十几路就可能开始排队。
第二招:多实例 + 负载均衡。一台机器扛不住,就上两台。每个实例各拉一个模型服务,用 Nginx 做轮询即可。配置也不复杂:
upstream laya_backend { server 127.0.0.1:8001; server 127.0.0.1:8002; } server { listen 8000; location / { proxy_pass http://laya_backend; proxy_read_timeout 3s; } }第三招:加缓存。判断器处理的内容里有很多是重复的,同一个用户反复问同一个问题、不同用户问相似问题,都会命中相似的意图。可以用向量库做 embedding 相似度匹配,命中缓存就直接返回结果,完全不需要进模型推理。这招对降低峰值压力特别有效。
第四招:超时熔断。所有判断请求都必须有超时时间,而且要在代码里写好降级路径。判断器超时就返回一个保守的默认值(比如按chat处理,或者直接走主模型兜底),保证主链路不因为判断器故障而完全不可用。
3. 部署 Jev:把“判断结果”钉死在格式上
3.1 两种接入方式:API 快速验证,本地私有化部署
Jev 和 Laya 的定位不同,所以部署方式也灵活一些。Jev 通常有官方 API,也可以本地部署。
API 接入是最快的方式,适合前期验证效果。环境变量存好密钥,HTTP 调用即可。有一个很关键的点:密钥要从环境变量里读,不要硬编码在代码里。我见过不少人的项目把密钥写在配置里然后推到仓库,这个风险不值得冒。
import os import requests JEV_API = os.environ["JEV_API_BASE"] JEV_KEY = os.environ["JEV_API_KEY"] def jev_check(payload: dict, schema: dict) -> dict: resp = requests.post( f"{JEV_API}/v1/validate", headers={"Authorization": f"Bearer {JEV_KEY}"}, json={"payload": payload, "schema": schema}, timeout=5, ) resp.raise_for_status() return resp.json()本地部署则适合对延迟敏感或者数据不出内网的场景。拿到模型文件后,用 vLLM 或者 llama.cpp 把服务跑起来。这里要特别注意:Jev 这类判断器必须开启 constrained generation(受约束生成),专业名词叫 guided decoding。它的作用是让模型在生成每个 token 的每一步都对齐 JSON Schema,保证输出“生成即合规”,而不是生成完了再想办法修复。
vllm serve jev-model --guided-json schema.json --max-model-len 4096这个机制是 Jev 和普通大模型最本质的区别。普通模型是解码完了才知道结果对不对,然后靠后处理硬掰;Jev 是在解码阶段就从机制上保证了输出符合结构要求,这才是它适合做判断器的根本原因。
3.2 工具参数三步走:路由、生成、校验兜底
在实际的 Agent 链路里,Jev 最常出现的位置是工具参数生成。拿天气查询举例,一个标准的工具 schema 可能是这样的:
import jsonschema from jsonschema import validate TOOL_WEATHER_SCHEMA = { "type": "object", "properties": { "city": {"type": "string"}, "date": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}, }, "required": ["city", "date"], "additionalProperties": False, }用户说“查一下北京后天的天气”,主模型不一定能准确给出日期。这时候我会走三步:
第一步,用 Laya 路由,判断确实是工具调用意图。第二步,让 Jev 先宽松生成一个候选参数,不要求一步到位。第三步,用 jsonschema 校验,不通过就让 Jev 按 schema 修复。
def build_tool_args(user_text: str) -> dict: # 第一步:Laya 判断是否调用工具 intent = route(user_text) if intent != "tool": return {} # 第二步:Jev 生成候选参数 candidate = jev_generate(user_text, TOOL_WEATHER_SCHEMA) # 第三步:校验兜底 try: validate(candidate, TOOL_WEATHER_SCHEMA) return candidate except jsonschema.ValidationError: return jev_fix(candidate, TOOL_WEATHER_SCHEMA)为什么先宽松生成再修复,而不是直接让 Jev 一步生成严格合规的参数?因为模型在生成过程中,如果约束太死,反而容易把必要字段丢掉。先给它一点发挥空间,再按 schema 修正缺失字段,综合成功率会更高。这个两步走的思路,我实测下来能把参数生成成功率从 85% 拉到 97% 以上。
3.3 在 Codex 类框架里挂一个工具代理
很多 Agent 开发框架,比如以 Codex 为代表的编码智能体,都提供了工具注册机制。工具调用前能不能加一道判断?工具返回后能不能加一道清洗?答案都是能,只需要在工具调用入口包一层代理。
def tool_proxy(name: str, args: dict): # 调用前校验参数 schema = TOOL_REGISTRY[name]["schema"] if not jev_check(args, schema): args = jev_fix(args, schema) # 调用真实工具 result = call_actual_tool(name, args) # 返回前做输出清洗 return jev_sanitize(result)这套代理模式的思路可以复用到几乎所有“以工具调用为核心”的 Agent 框架里,不局限于某一个具体框架。工具注册表中存了每个工具的入参 schema 和出参校验规则,Jev 在调用前后各做一次判断,整个链路的稳定性就上来了。
这里要提醒一句:不是所有工具都需要 Jev 把关。对于参数简单、错误影响小的工具,走规则校验就够了;只有那些参数复杂、一旦传错会引发严重后果的工具,才值得让 Jev 介入。判断器也是成本,别滥用。
4. Laya 和 Jev 到底怎么选:先算延迟账,再谈架构
4.1 先算一笔延迟和成本账
选型最怕什么都不看,直接说“哪个效果好就上哪个”。判断器挂在主链路上,效果当然重要,但延迟和成本同样硬性。
我算过一笔账:一次 Agent 请求里,主模型生成部分通常要 2 到 5 秒,这在用户体感上已经是一个“思考中”的过程了。判断器每加一次判断,如果只消耗 100 到 300 毫秒,用户是感知不出来的;但如果一次判断要 1 到 2 秒,用户就会发现整个链路明显变慢。
| 环节 | 单次延迟 | 单次成本 | 维护量 |
|---|---|---|---|
| 主模型 | 2~5 秒 | 高 | 中 |
| Laya 判断 | 0.1~0.3 秒 | 低 | 中 |
| Jev 校验 | 0.3~1.5 秒 | 中 | 低 |
所以我的建议是:延迟是硬约束,先把延迟账算清楚,再考虑准确率。如果业务峰值要求一次判断不超过 300 毫秒,那 Jev 就不适合放在高频路径上,应该把更轻量的 Laya 放在前面做粗筛,只有粗筛命中的请求才继续走 Jev 的精细校验。
4.2 三种可复用的架构组合
根据我的实际经验,判断器的架构组合基本可以归成三类。
组合 A:主模型 + Laya 路由。适合对话类 Agent、知识问答系统,工具调用不频繁,大多数请求直接对话就能解决。Laya 在这里的作用是把“偶尔需要用工具”的请求挑出来,其余请求直接给主模型回复。
组合 B:主模型 + Laya 路由 + Jev 校验。适合工具调用频繁、参数准确性要求高的生产系统。用户请求进来先由 Laya 判断要不要动工具,动了工具之后由 Jev 确保参数合规、结果合规。这个组合稳定性和效果最好,也是我目前的主力架构。
组合 C:规则优先 + Jev 兜底 + 主模型兜底。适合数据流水线、表格处理这类确定性要求极高的场景。能写规则的先写规则,规则覆盖不到的交给 Jev,Jev 都不行再让主模型处理。这个组合成本最低,但前期要花时间整理规则,适合规则边界清晰的业务。
三种组合没有绝对优劣,只有适不适合。小项目上组合 A 就够,硬上组合 B 只会增加链路复杂度和排查成本。
4.3 我的选型结论:够用就好,别给链路加戏
我做过几个不同类型的 Agent 项目,选型结果可以作为参考。
信息检索类 Agent,工具多、调用频繁,用户问题千奇百怪,最后选了组合 B。Laya 先做粗粒度路由,Jev 在每次工具调用前做参数校验,整体准确率比只用主模型高了接近 10 个百分点。
代码生成辅助 Agent,主要靠主模型自身能力,工具调用集中在文件读写和命令执行,选了组合 A。Laya 只需要判断当前请求是继续对话还是执行命令,简单直接。
数据处理系统,对确定性的要求极高,我列了很多规则,然后用 Jev 处理规则边界处的模糊情况,基本是组合 C。这套组合跑了一段时间,稳定性最好,出问题最少。
说句实在话,判断器不是越强越好。模型越大,延迟越高,维护成本也越高。很多人一上来就想着上最好的、最全的,结果链路变慢、日志爆炸,最后一层都没跑稳。我的建议是:先用最小可用判断器把链路跑通,再按实际瓶颈逐步加层。
5. 常见问题与排查技巧实录
5.1 判断器输出“一大堆废话”而不是 JSON
这是最常遇到的问题。判断器应该输出一个干净的 JSON,结果它输出“好的,我来帮你分析一下用户的需求,根据我的理解,这个输入应该被分类为______”之类的一大段废话。
排查顺序很重要。先看接口返回的原文,确认模型到底输出了什么,不要直接拿解析后的结果去猜。然后检查两件事:第一,temperature是否设成了 0;第二,推理服务是否真的开启了 JSON 模式。Ollama 的format="json"参数是接口层的开关,如果这个开关没生效,模型大概率会自由发挥。
如果模型本身不支持受约束生成,还有一个兜底办法:用正则把 JSON 片段抽出来。我在很多线上模块里都用了这个兜底逻辑:
import re import json def extract_json(raw: str) -> dict: m = re.search(r"\{.*\}", raw, re.S) if not m: raise ValueError(f"no json in raw: {raw[:200]}") return json.loads(m.group())注意,正则兜底是最后的手段,不是第一手段。第一手段永远是把约束做在前面,让模型从一开始就按格式生成。
5.2 本地部署 OOM / 启动失败排查
本地部署判断器,最常见的问题就是显存不够。CUDA out of memory一出现,很多人第一反应是换更大的卡,但其实很多情况只需要调整参数就能解决。
我整理了一个排查表,可以直接对照:
| 症状 | 原因 | 解法 |
|---|---|---|
| OOM | 模型量化级别太大 | 换 q4/q3 量化,或换更小尺寸模型 |
| 启动失败 | 显存不足 | 调小--max-model-len,减少 KV Cache 占用 |
| 响应慢 | CPU 推理 | 换 GPU,或减少并发上限 |
| 并发排队 | 单实例处理能力不够 | 多实例部署 + 负载均衡 |
实际测试下来,8G 显存跑 7B q4 模型非常吃力,上下文长度稍微拉长就 OOM;16G 显存跑 7B q4 就比较舒服了。如果手头只有 8G 同等的卡片,建议直接把上下文限制在 4K 以内,否则启动都费劲。
5.3 同一输入结果却不一样?先看随机性
判断器的输出如果时好时坏,绝大多数是随机性没有关干净。temperature没设为 0,模型每次采样结果都会有差异;有些推理框架默认还会随机初始化 seed,也会影响结果。
排查方法很简单,写个单元测试,同一个输入连续跑 20 次,看结果分布。如果分布不稳定,检查三处:temperature=0、top_p=1、固定 seed(比如seed=42)。判断器是确定性组件,不是创作组件,任何随机性都可能是线上问题的大坑。
5.4 我惯用的断点排查三步法
判断器接入 Agent 链路之后,定位问题比解决问题更难。我自己的排查习惯是三步走。
第一步,绕开主链路,直接单测判断器接口。先用一个已知的输入直接调用判断器,确认它本身是否稳定。这一步能快速分清是判断器的问题还是主链路的问题。
第二步,加 trace_id。每次 Agent 请求都生成一个唯一标识,把主模型 prompt、判断器输出、工具调用记录全部带上 trace_id 写入日志。排查问题时按 trace_id 回放整条链路,一眼就能看出是哪个环节出了偏差。
第三步,回放对比。把线上日志里的判断器输入输出导出来,和期望结果做对比,逐条分析。很多时候问题不是模型不行,而是 prompt 里的示例太少了,或者分类定义有歧义,补充几个样本就能解决。
5.5 常见问题速查表
最后放一个速查表,几乎覆盖了我遇到的大部分判断器问题:
| 问题 | 现象 | 建议 |
|---|---|---|
| 输出格式乱 | 返回多了解释性文字 | 开启 JSON 模式,加正则兜底 |
| 判断时好时坏 | 同一输入多次结果不同 | temperature=0,固定 seed |
| 超时 | 请求排队 | 限流 + 超时熔断 + 多实例 |
| 显存不够 | OOM / 启动失败 | 换小量化、降上下文长度 |
| 分类不准 | 意图判断错误 | 补充 few-shot 样本,细化分类定义 |
最后说点个人体会。加了判断器并不等于万事大吉,它只是把“不确定性”从主模型那里拆出来单独管理。我一开始也走过弯路,把判断器做得特别重,结果链路慢了、日志爆炸了,后来砍到只剩 Laya 一层路由,反而稳定很多。所以我的建议是:从小做起,先把一个判断点跑透,再决定要不要加第二个。另外分享一个小技巧:给每一次判断请求都带上 trace_id,链路出问题的时候按 id 回放,真的能省一半的调试时间。