最近调 Agent 系统的时候,我栽了不少跟头。最典型的一个:用户让 Agent 查一笔订单的物流信息,Agent 绕来绕去,最后居然打算调用“创建退款”的工具。模型本身没有错,它只是把用户意图和工具参数搞混了。真正缺的,是一个在动手之前踩刹车的组件——我给这种东西起了个名字:判断器。
判断器是一个独立的决策节点,Agent 的每个动作在真正执行前都要先过它这一关。围绕这个思路,我先后对比了 Laya 和 Jev 两套方案,也踩了一堆部署和选型的坑。这篇文章就把这些实战经验完整记录下来,给正在做 Agent 治理、工具调用安全,或者想本地部署判断器的朋友做个参考。
1. 判断器到底在解决什么问题
1.1 Agent 失控,通常不是模型的锅
很多人以为 Agent 出问题是大模型能力不够,比如智商低、理解差。但我在实际项目里发现,真正翻车的场景往往是“模型什么都懂,但没有边界感”。一个能写代码、能调用 API 的 Agent,和一把没有保险栓的电锯一样危险。
典型的 Agent 执行链路是:
任务解析 -> 规划 -> 工具选择 -> 工具调用 -> 观察结果 -> 继续循环 -> 输出大多数安全问题发生在“工具选择”和“工具调用”之间。模型选择了错误的工具,或者正确工具配了危险参数,这时候如果没人拦一下,后果就不可控了。判断器就放在这个位置,相当于给执行链路加了一道审批闸门。
我总结过常见的失控类型:
- 工具误用:把“查询账单”理解成“发起退款”,把“读文件”理解成“删除文件”。
- 死循环:Agent 反复调用同一个工具,参数不变、结果不变,白白消耗 token。
- 越权操作:删除数据、发送邮件、对外写入消息,这类动作一旦执行很难撤销。
- 低质量输出:Agent 跑了半小时,返回的结果和用户最初目标完全对不上。
这些问题里,只有一部分是模型推理能力问题,剩下大多是缺少判断机制。你可以把 Agent 想象成刚入职的实习生:活能干,但不知道什么时候该请示、什么时候该停手。判断器就是那个负责审批的主管。
1.2 判断器的两条路线:规则优先还是模型判断
判断器不是某种单一技术,而是一类组件的统称。我把它分成两条路线。
第一条是规则/启发式路线。用正则、白名单、黑名单、阈值、状态机这些东西做拦截。优点是延迟低、完全可解释、误判可以精准修正;缺点是很傻,碰到语义复杂的话就失灵。比如你写一条规则“凡是调用删除接口都拒绝”,那 Agent 想删临时文件也被拒,业务就崩了。
第二条是模型判断路线。用一个小的分类模型,或者一个大模型作为 judge,对 Agent 的动作做语义级别的评估。它能理解“这个删除操作针对的是临时目录,可以放行”,“那个查询动作带上了支付参数,有问题”。缺点是重、慢、可能有幻觉,自己也会出错。
实际上成熟方案不会只用一种。我在项目里同时用了两套东西,恰好对应两条路线:Laya偏轻量动作审查,适合做第一道闸;Jev偏深度评估,适合做复杂判断。接下来分别展开讲。
2. Laya:轻量级动作审查器
2.1 Laya 的定位和输入输出
Laya 在我这边定位是“轻量级动作审查器”。它不做复杂的任务规划,也不替 Agent 写代码,只回答一个问题:这个动作能不能做。
Laya 的模型量级不大,量化后可以跑在边缘设备上,比如 Jetson Orin、RK3588,甚至纯 CPU 环境也能跑。这不是因为它弱,而是设计目标就是低延迟、高吞吐。它要挡的是“明显不该做”的动作,而不是去评判整个任务的好坏。
它的输入设计很关键。我在接入的时候没有把完整对话历史丢进去,而是做了一个结构化输入:
{ "action": "create_refund", "action_params": { "order_id": "20241101-001", "amount": "499.00" }, "context_summary": "用户要求查询物流信息,未提及退款", "sensitive_flags": ["payment", "refund", "write"] }输出是固定 JSON:
{ "label": "reject", "score": 0.92, "reason": "用户目标是查询物流,未授权退款操作" }label 分三档:accept、needs_review、reject。score 表示置信度。把输入输出结构定死,后续接任何 Agent 框架都方便。
2.2 部署 Laya 的完整步骤
部署 Laya 我用的是 GGUF 量化版。模型本身如果直接跑 FP16,边缘设备扛不住,量化到 Q4_K_M 之后,体积和显存占用都下来了,精度损失对判断任务来说基本可以接受。
第一步是准备模型文件。从模型仓库拉取量化后的 GGUF 权重,找 Q4_K_M 或者 Q5_K_M,我这边实测 Q4_K_M 是性价比比较高的档位。文件大小大概在 3GB 到 5GB 之间,取决于模型原始参数量。
第二步用 llama.cpp 起一个本地推理服务:
./server \ -m laya-7b-q4_k_m.gguf \ --host 127.0.0.1 \ --port 8081 \ -c 4096 \ --temp 0.1-c 4096是上下文长度。判断器不需要读完整对话,4K 足够。--temp 0.1是为了让输出尽量稳定,判断任务不应该有创造性。
第三步写一个 Python 客户端把接口包一层:
import requests class LayaClient: def __init__(self, endpoint="http://127.0.0.1:8081"): self.endpoint = endpoint def review(self, action, params, context_summary, sensitive_flags): payload = { "action": action, "action_params": params, "context_summary": context_summary, "sensitive_flags": sensitive_flags } resp = requests.post(f"{self.endpoint}/completion", json={ "prompt": build_prompt(payload), "temperature": 0.1, "max_tokens": 128, "stop": ["\n"] }) return parse_response(resp.json())这里我没有直接用 llama.cpp 原生接口,而是中间加了一层 Python。原因很简单:Agent 主链路是 Python 写的,统一用 Python client 能省掉很多类型转换和错误处理的麻烦。
显存估算方面,可以按这个公式粗略计算:
模型显存 ≈ 权重文件大小 + KV Cache + 运行时开销Q4_K_M 的 7B 模型权重约 4.2GB,4K 上下文的 KV Cache 大约 0.5GB,再加上推理框架自身占用,总需求大概 5GB 到 6GB。这意味着 8GB 显存的 Jetson Orin 能跑,16GB 内存的 RK3588 开发板也能通过 CPU 方式跑,只是速度会慢一些。
2.3 怎么接入 Agent 而不把链路拖垮
位置很重要。判断器应该放在“工具调用参数组装完成之后”“真正执行 API 调用之前”。如果放在 Agent 模型内部,那你换一个模型就得重写;如果放在太外层,比如放在用户请求入口,又拦不到中途产生的工具调用。
我用的接入方式是写一个装饰器,统一包住所有工具函数:
def guarded_tool(func): @wraps(func) def wrapper(*args, **kwargs): action = func.__name__ params = kwargs result = laya_client.review(action, params, build_context_summary()) if result.label == "reject": raise ToolRejected(result.reason) if result.label == "needs_review": return escalate_to_human(action, params, result.reason) return func(*args, **kwargs) return wrapper这里有个非常重要的设计:被 reject 之后,不是直接把错误抛给用户,而是把 reason 变成一条“观察结果”返回给 Agent,让 Agent 重新规划。比如 Agent 要创建退款被拦了,reason 是“用户目标是查询物流”,Agent 看到之后就会修正成查询接口。这比硬切断体验好很多。
还有兜底策略。判断器本身的延迟如果超过阈值,不能让它把 Agent 整个拖死。我的做法是分场景:高风险操作(删除、转账、发消息)超时默认拦截,低风险操作(查询、读取)超时默认放行,同时记录一条 warning。默认全拦会误伤正常流程,默认全放又会失去保护意义,必须按风险分级。
3. Jev:重判断模型,负责多轮评估
3.1 Jev 擅长的事情
如果说 Laya 是“交警”,那 Jev 更像“项目评审”。它不盯着单次动作,而是评估整个 Agent 行为序列是否合理。
Jev 能处理三类问题:目标一致性、冲突检测、结果质量评估。
目标一致性是指 Agent 做了一连串操作之后,到底有没有在朝用户最初的目标前进。比如用户说“分析这份销售数据并做图表”,Agent 中途开始调整数据源、删列、改格式,这些单看都没问题,但放在一起可能已经偏离目标。Jev 能结合多轮上下文给出“当前行为与原始目标匹配度 70%”这样的判断。
冲突检测是看多步操作之间是否互相矛盾。Agent 先删除了某个临时表,后面又去查这个表,Jev 能发现这种前后冲突,提醒 Agent 修正路径。
结果质量评估则更像质检。Agent 生成了一段 SQL、一份摘要、一个图表配置,输出前用 Jev 检查一下“是否满足用户需求、有没有明显错误、缺不缺关键信息”。社区里有人用 Jev 构建数据系统,本质就是每完成一个数据处理步骤,让 Jev 判断一下中间结果还能不能往下走,省去人工盯过程。
3.2 Jev 的获取与部署
Jev 的权重和 API 不是随手就能拿到的,很多项目需要走申请流程。这是模型厂商控制使用场景的常见做法,申请时说明用途、部署环境、数据量,一般几天内会有反馈。拿到之后有两种部署模式:托管 API 和本地权重。
托管 API 最省心,直接在代码里配一个 endpoint 和 key 就能调用。但数据要过第三方,敏感业务不太合适。
本地部署我主要用 vLLM。因为 Jev 的输入输出都比较长,推理引擎必须支持 continuous batching,否则并发一高就卡死。Windows 上部署时需要注意几点:Python 3.10 以上、CUDA 12 环境、显存足够。以 34B 量级模型为例,bf16 精度大约需要 70GB 显存,单张 80GB 的卡能跑,或者用两张 40GB 卡做 tensor parallel。
启动命令大致如下:
python -m vllm.entrypoints.openai.api_server \ --model jev-model \ --dtype bfloat16 \ --tensor-parallel-size 2 \ --max-model-len 32768 \ --port 8000启动之后它就暴露一个 OpenAI 兼容接口,调用方式和 ChatCompletion 一样。这对接现有框架非常方便。
在 Codex 这类 Agent 工具里用 Jev,我采取的方式是把 Jev 封装成一个自定义判断插件。Codex 本身不强制判断器,但它开放了接口让外部逻辑注入。配置层面大概是:
custom_judge: name: jev endpoint: http://127.0.0.1:8000/v1 review_after_each_tool: true review_conditions: - "tool_result_contains_error" - "proposed_tool_is_sensitive" - "task_branch_depth_greater_than_3"这个配置表示每次工具执行后都让 Jev 看一眼,但为了控制成本,只有出现错误、敏感操作或者分支深度太高时才做完整评估。
3.3 Jev 的参数与调用技巧
Jev 这类重模型,调用参数直接决定输出质量。我踩过几个坑,总结成三条经验。
第一,temperature 必须设成 0,或者直接关闭采样。判断任务是确定性问题,不需要“创造性”。temperature 高了,同一个动作可能这次判通过,下次判拒绝,用户会被搞疯。
第二,强制输出 JSON 结构。我在 prompt 里明确要求只在固定 schema 的 JSON 里输出,不允许输出任何解释性废话。解析失败的样例我会扔进 few-shot,模型会很快学会。
{ "verdict": "allow | deny | review", "confidence": 0.0, "evidence": ["用户未授权退款", "工具参数包含金额"] }第三,给 Jev 提供轻量级“评审手册”。与其让它自由发挥,不如把评分标准写清楚:什么情况必须拒绝、什么情况需要人工、什么情况可以放行。few-shot 示例放三到五条就够了,太多反而会干扰判断。
3.4 分层判断:把 Laya 和 Jev 组合成一套管线
单用一个判断器,要么太快但不准,要么太准但太慢。我的做法是分层。
第一层全量走 Laya。因为它便宜、快,任何 Agent 动作都先过一遍,挡掉明显的高风险动作和格式错误。第二层只处理“需要关注”的样本。Laya 的判定结果如果是 needs_review,或者 Agent 执行过程中出现了多次重试、报错、分支异常,再由 Jev 做深度判断。
伪代码大概是:
for action in agent_actions: quick = laya_client.review(action) if quick.label == "accept": execute(action) elif quick.label == "reject": raise ToolRejected(quick.reason) else: deep = jev_client.review(action, full_context) if deep.verdict == "allow": execute(action) elif deep.verdict == "deny": raise ToolRejected(deep.evidence) else: escalate_to_human(action, deep.evidence)这套分层管线跑下来,Laya 拦截了大约 70% 的明显问题,Jev 只处理剩下 30% 的疑难杂症。整体延迟增加控制在 20% 以内,但安全事件少了非常多。
4. 选型与部署决策:Laya 还是 Jev,或者都要
4.1 一张表帮你快速决策
判断器选型不能只看模型能力,要结合硬件、延迟、数据敏感度、运维成本。我把决策因素整理成一张速查表:
| 场景 | 延迟要求 | 硬件预算 | 数据敏感度 | 推荐方案 |
|---|---|---|---|---|
| 边缘设备、IoT | 毫秒级 | 低(Jetson/RK3588) | 高 | 仅 Laya |
| 普通 API 网关 | 百毫秒级 | 中(单卡 GPU) | 中 | 仅 Laya |
| 复杂业务工作流 | 秒级可接受 | 高(多卡 GPU) | 高 | Laya + Jev 分层 |
| 快速原型验证 | 不敏感 | 无 GPU,依赖 API | 低 | 托管 Jev API |
| 数据系统、数据分析管线 | 秒级 | 中高 | 高 | Laya 过滤 + Jev 阶段性评估 |
从这个表也能看出来,判断器不是越重越好。很多场景只需要 Laya 就够了,强行上 Jev 反而把延迟和成本都拉上去。
4.2 Agent 并发怎么扛:判断器也不能掉链子
很多人只关心 Agent 主模型怎么扛并发,忽略了判断器也可能成为瓶颈。实际上判断器是串联在 Agent 链路里的,它一卡,整个 Agent 都卡。
计算并发能力的公式很简单:
单实例 QPS ≈ 并发数 / 单请求耗时举个例子,Jev 在 A100 上处理一个复杂判断请求,平均耗时 1.5 秒。如果只开单线程跑,QPS 只有 0.67,基本不可用。开到 8 并发,QPS 到 5.3,才开始有点实战价值。业务侧如果要求 50 QPS,那就得部署 10 个实例,或者用 continuous batching 把吞吐打上去。
Laya 因为模型小,情况好很多。llama.cpp 服务在 CPU 上单次推理可能只有 20 到 50 毫秒,开多进程做负载均衡,单机扛几百 QPS 问题不大。
还有一个容易忽略的优化:缓存。Agent 的动作如果重复率很高,比如很多人都用同一个“查询订单”工具,上下文也相似,判断结果完全可以缓存。我用 Redis 做了一层缓存,key 是“动作名 + 参数摘要哈希 + 上下文摘要哈希”,命中率大约 30%,延迟直接从 20 毫秒降到 1 毫秒。
4.3 本地部署和 API 托管怎么选
本地部署和 API 托管不是对立关系,而是资源约束下的取舍。
本地部署适合三类情况:数据不能出内网、硬件已经到位、需要长期大规模调用。缺点是运维成本高,模型更新、bug 修复、性能调优全得自己来。我团队里没有专门的推理运维工程师,所以本地部署只放了 Laya,Jev 直接用托管 API 兜底。
API 托管适合快速原型和小流量场景。不需要管 GPU,不用处理 CUDA 版本冲突,按量付费。但单量上来之后,费用会变得很吓人。一个判断请求如果消耗几千 token,一天跑十万次,日成本可能就顶一台 GPU 的月租了。
我的建议:业务规模还不确定的时候先走 API,验证判断器确实有效、拦截率稳定之后,再考虑把流量大的部分迁到本地。
4.4 不同 Agent 框架的接入差异
接入判断器之前,先要搞清楚 Agent 和 harness 的区别。Agent 是那个能推理、能调用工具的模型,harness 是控制 Agent 循环执行的框架外壳。判断器应该挂在 harness 层,而不是塞进 Agent 模型内部。
这样做的最大好处是解耦。今天用 DeepSeek 做 Agent 底座,明天换成别的模型,harness 不动,判断器也不用动。我在 LangGraph 里接判断器就是在每个节点执行的守卫函数里调 Laya,在 OpenAI function calling 场景里就是在 tool call 之前加一道拦截,自研的 Codex 类工具也是在工具执行网关里挂 Jev。
统一的接入抽象可以做成这样:
class ReviewProvider(Protocol): def review(self, action, params, context) -> ReviewResult: ...LayaProvider 和 JevProvider 都实现这个接口,Agent 框架只依赖接口,不依赖具体实现。这样后面想换判断器、加判断器,都是配置级别的事情。
5. 实操中的踩坑记录与排查技巧
5.1 判断器误杀太多怎么办
判断器上线第一周,最常听到的反馈是“这玩意儿把正常请求也拦了”。误杀是判断器最严重的问题,因为它直接伤害用户体验。
我踩坑之后的做法是:先不拦截,只看结果。判断器全量接入,但 reject 的时候不阻断,而是打日志。跑三天之后统计:
| 动作 | 判断器拒次数 | 人工复核正确次数 | 准确率 | 误杀次数 |
|---|---|---|---|---|
| 查询订单 | 11 | 3 | 27% | 8 |
| 创建退款 | 42 | 39 | 93% | 3 |
| 删除临时文件 | 57 | 55 | 96% | 2 |
统计完发现,“查询订单”被误杀特别多。原因是上下文摘要截断策略有 bug,把用户历史里的退款意图错误地带到了当前查询场景。修正截断逻辑之后,准确率立刻上来了。
在调整阈值时我也发现,不要只看总准确率,要分动作单独看。危险动作哪怕准确率 90% 都值得拦,普通查询动作准确率不到 95% 就别上生产。
5.2 上下文太长导致延迟爆表
Jev 这类重模型对输入长度特别敏感。输入 8K token 可能只要 0.8 秒,输入 32K token 可能要 3 秒以上。如果每次判断都塞完整对话,延迟直接爆表,Agent 体验接近不可用。
我采用的截断策略分三层:
- 保留最底层的 system prompt,里面写清楚任务边界。
- 保留用户最初的那条原始指令,这是目标一致性的锚点。
- 保留最近 3 轮对话和当前动作相关的工具结果,去掉历史里的无关中间过程。
截断之后,Jev 的输入长度稳定控制在 4K 到 6K token,延迟下降了大半,判断准确率反而提升了。原因是信息密度更高,噪声更少。
5.3 判断器自己也会产生幻觉
模型判断器不是神,它一样会一本正经地胡说八道。我遇到过 Jev 把一次正常的数据库查询判成“敏感操作”,原因竟然是 prompt 里出现了“银行”两个字,它自动关联到了金融风险。
对抗幻觉的方法有三个。一是给评分标准加上严格定义,比如“只有满足以下至少一项才允许 reject:涉及资金变动、涉及删除/覆盖、涉及外发消息”。二是让模型输出 evidence,必须引用具体上下文片段,禁止空泛描述。三是做对抗测试,上线前准备 100 条正常请求和 100 条恶意请求,混在一起跑,准确率达不到 95% 不上线。
还有一招很实用:在判断结果里加一个confidence字段。置信度低的判断结果不再自动执行,而是进入人工队列。宁可多一次人工确认,也不要放掉一次危险操作。
5.4 边缘端部署的特殊问题
在 Jetson Orin 上部署 Laya,最痛苦的是从 PyTorch 格式转到 TensorRT 引擎。直接用开源的 GGUF 加载是能跑,但推理速度一般,跑不满 Orin 的算力。我后来用 TensorRT 重新编译图,把延迟压到了 10 毫秒以内。
RK3588 上的情况更麻烦。它的 NPU 工具链只支持特定格式,转换过程容易出现算子不支持的问题。我的经验是先把模型在 CPU 上跑通,验证输出结果没问题,再切 NPU。不要一上来就做格式转换,否则你分不清是模型效果差还是转换精度丢失。
边缘端还有一个容易忽略的问题:内存紧张。Q4 量化模型虽然小,但推理时的临时变量、KV Cache、运行时框架都需要额外内存。8GB 内存的开发板如果同时跑 Agent、OCR、判断器,很容易 OOM。解决办法是按优先级做内存预留,或者直接用更小的量化档位,比如 Q2_K,但精度就不好说了。
5.5 澄清一个概念:harness 和 Agent 的区别
和不少朋友交流时发现,很多人把 Agent 和 harness 混为一谈。Agent 是指模型本身具备的推理和工具调用能力,harness 是包在模型外面的一整套控制逻辑,包括任务循环、上下文管理、工具管理、错误恢复。
判断器天然属于 harness 层。它管的是 Agent 的行为,不是模型的参数。想明白这一点,你就能理解为什么判断器必须独立部署,而不是把判断逻辑写进 system prompt。曾有人在 prompt 里写“不要调用危险工具”,模型偶尔还是闯祸,因为提示词对大模型来说是软约束,而判断器是硬约束,两者不在一个层级。
6. 一个可以抄作业的最小接入模板
6.1 目录结构
篇幅有限,我直接给一个最小可跑的工程结构,你照着复制就能用。
agent-judge/ ├── laya_client.py ├── jev_client.py ├── judge_gateway.py ├── main.py └── config.yamllaya_client 封装轻量判断器,jev_client 封装重判断器,judge_gateway 做分层路由,main.py 起一个 FastAPI 服务,config.yaml 放阈值和开关配置。整个工程不依赖任何特定 Agent 框架,任何语言写的 Agent 都可以通过 HTTP 调用它。
6.2 核心代码
先看 judge_gateway 的核心逻辑:
class JudgeGateway: def __init__(self, config): self.laya = LayaClient(config["laya_endpoint"]) self.jev = JevClient(config["jev_endpoint"]) self.default_policy = config.get("default_policy", "reject") self.timeout = config["timeout_seconds"] def review(self, action, params, context_summary): try: quick = self.laya.review( action, params, context_summary, timeout=self.timeout ) except TimeoutError: return self.apply_timeout_policy(action) if quick.label == "accept": return ReviewResult("allow", quick.score, quick.reason) if quick.label == "reject": return ReviewResult("deny", quick.score, quick.reason) # needs_review 时交给重判断器 deep = self.jev.review(action, params, context_summary) return ReviewResult(deep.verdict, deep.confidence, deep.evidence)timeout 策略单独抽出来:
def apply_timeout_policy(self, action): if action in high_risk_actions: return ReviewResult("deny", 1.0, "timeout_high_risk") if self.default_policy == "reject": return ReviewResult("deny", 1.0, "timeout_default_reject") return ReviewResult("allow", 0.0, "timeout_default_allow")main.py 里暴露一个 HTTP 接口:
@app.post("/v1/review") def review(request: ReviewRequest): result = gateway.review( request.action, request.params, request.context_summary ) return {"verdict": result.verdict, "reason": result.reason}Agent 框架只需要在工具调用前POST /v1/review,拿到deny就阻止执行。整个接入成本很低,不需要改 Agent 模型本身。
6.3 上线前检查清单
我把最后检查清单列在这里,每一条都是踩坑换来的:
- 判断器超时后是拒绝还是放行?按动作风险级别区分,不能一刀切。
- 判断器挂掉时有没有降级开关?我建议默认降级为“人工确认”,不要自动放行。
- 每次判断结果是否完整落日志?不落日志等于没做判断,后面没法优化。
- 是否有监控面板看拦截率、误杀率、延迟?至少要有三个指标:P50 延迟、拒绝率、人工升级率。
- 有没有人工审阅入口?needs_review 的判断结果必须能流转到人工系统。
- 判断器 prompt 或模型升级后,是否跑过回归测试?别改完就上线,前一天好用的模型可能第二天就抽风。
个人在实际操作中的体会是:判断器这东西,真不是越重越好。先用规则加上 Laya 挡住八成明显的坑,等业务复杂度上来、出现真正的多步冲突时,再上 Jev 做深度评估,既能控制成本,又能把安全边界打磨得比较稳。最后分享一个小技巧:把判断器的每一次决策都落成结构化日志,攒一个月数据之后拿去做二次微调或者阈值优化,效果比手动拍脑袋调参数好得多。