1. 从“JSON约束”到“语义可靠”:一个被忽视的Agent核心挑战
最近在折腾一个基于大语言模型的订单处理Agent,相信很多同行都干过类似的事:定义一个JSON Schema,告诉LLM“请严格按照这个格式输出”,然后满怀期待地等待一个结构完美的结果。一开始,效果确实不错。订单号、商品列表、用户信息,所有字段都规规矩矩地躺在JSON对象里,格式校验器(比如jsonschema)轻松通过,数据管道顺畅流转。这让我一度以为,用Schema约束LLM输出,是构建可靠Agent的“银弹”。
但很快,现实就给了我一记闷棍。在一个处理“用户取消订单并申请仅退款”的场景中,Agent返回的JSON完全符合Schema:action字段是refund,order_id正确,reason字段也填了“用户取消”。从格式上看,无懈可击。然而,当我们把这条记录推送到下游的财务系统时,却触发了风控警报。原因在于,这个订单的商品是虚拟卡券,早已被核销使用。根据业务规则,此类订单根本不允许“仅退款”,只能走“退款并退货”(实际上虚拟商品也无法退货)或者协商补偿的流程。Agent输出了一个语法正确但语义荒谬的指令。
这就是标题所指的“当JSON不够用时”。我们精心设计的Schema,就像一个语法检查器,它能确保输出的是“名词+动词”的句子结构,却无法判断这个句子在现实世界中是否“讲得通”。Schema保障了格式的合规性(Syntactic Correctness),却对语义的合理性(Semantic Reliability)无能为力。对于一个真正要在生产环境中运行的LLM Agent来说,后者才是决定其是否可靠、能否被信任的关键。本文将深入探讨这个从“格式正确”到“语义可靠”的鸿沟,结合我踩过的坑,分享一套构建语义可靠Agent的实战思路与评估框架。
2. 拆解“语义不可靠”:Schema约束下的三类典型陷阱
为什么一个格式完美的JSON会蕴含语义错误?这需要我们先跳出纯技术的视角,从Agent与真实世界交互的层面来理解。根据我的实践观察,语义不可靠性主要潜伏在以下三个层面,它们都逃过了Schema的校验网。
2.1 陷阱一:外部知识缺失与静态Schema的动态矛盾
这是最常见的一类问题。我们的JSON Schema往往是静态定义的,它规定了字段的名称、类型、是否必需,甚至枚举值。例如,一个电商订单Schema里可能有status字段,枚举值为["pending", "paid", "shipped", "delivered", "cancelled"]。
问题在于,真实世界的业务规则是动态、复杂且充满例外的。Schema知道status可以是cancelled,但它不知道:
- 前提条件:订单只有在
paid之后且未shipped之前,才能被设置为cancelled。一个pending的订单直接变cancelled可能没问题,但一个已delivered的订单再变cancelled就是语义错误。 - 关联约束:将
status改为cancelled,可能必须同步将refund_amount字段设为订单总金额,并且cancellation_reason不能为空。这些跨字段的约束,在扁平化的Schema中很难优雅表达。 - 外部状态:订单能否取消,可能还取决于库存状态(是否已拣货)、促销活动规则(是否使用了不可退款优惠券)等外部系统状态。这些信息根本不在本次LLM调用的上下文或Schema定义中。
我的踩坑记录:我们曾有一个
apply_coupon的Action,Schema要求提供coupon_code。Agent在一次会话中成功地输出了{"action": "apply_coupon", "coupon_code": "WELCOME2024"},格式完全正确。但实际这个优惠券早已过期。Schema不关心代码是否有效,它只关心coupon_code是个字符串。结果就是用户端提示“优惠券应用成功”,但结算时金额没变,体验割裂。
解决方案思路:Schema需要从“数据格式合同”升级为“业务规则契约”。除了基本类型,可以尝试引入轻量级的逻辑表达式描述(如通过description字段提示规则,或使用如json-schema-faker结合自定义规则)。更根本的,是让Agent在输出前,拥有一个“事实核查”的环节,能通过查询API、知识库来验证其输出动作的前提条件是否满足。
2.2 陷阱二:上下文误解与指令漂移
LLM是概率模型,擅长联想和生成,但也容易受到上下文干扰。在长对话或多轮交互中,Agent可能会“忘记”核心任务,或对用户模糊的指令产生合理但错误的解读。
例如,用户说:“把刚才我看的那个贵的耳机从购物车里删了吧,换成那个便宜的。” Agent需要解析出两个动作:1. 从购物车删除商品A。2. 向购物车添加商品B。一个简单的update_cartAction Schema可能只支持单个商品的操作。Agent可能会:
- 错误地只执行删除,忽略了“添加”。
- 或者,它输出了一个自认为合理的复杂JSON,试图在一个动作里完成两件事,却违反了Schema。
更微妙的情况是“指令漂移”。用户可能说:“帮我订一张明天去上海的机票。” Agent输出符合Schema的订票指令。用户接着说:“对了,要早上的。” 在第二轮,Agent需要更新之前的指令,将出发时间限定在上午。一个设计不佳的Agent可能会输出一个全新的、包含所有信息的订票指令,而不是一个“更新出发时间”的补丁指令。如果下游系统处理的是完整指令的覆盖,这可能没问题;但如果下游系统是增量处理,这就会导致语义混乱(是订两张票还是修改一张?)。
实操心得:我们引入了“对话状态跟踪”模块。这个模块独立于Action Schema,专门维护一个本次对话的核心任务框架(比如
intent: book_flight,slots: {destination: Shanghai, date: tomorrow, time: undefined})。Agent的每次输出,不仅是对用户当前句子的反应,更是对这个对话状态的读取和更新。输出Action时,会参考当前状态是“新建任务”还是“修改任务”,从而选择不同的输出策略。这显著减少了因上下文断裂导致的语义错误。
2.3 陷阱三:模糊输入的“创造性”解析与安全边界
用户输入常常是模糊、不完整或带有歧义的。LLM被要求“必须输出JSON”,于是它会在Schema的框架内,发挥“创造性”来填补空白,而这往往踏入语义雷区。
- 默认值填充的陷阱:用户说“取消订单”,但没提订单号。Schema中
order_id是必填字段。LLM可能会从对话历史中“猜测”一个订单号填进去,或者更糟糕,生成一个符合格式的随机字符串(如“NULL”或“last_order”)。从Schema看,order_id字段有值,类型是字符串,校验通过。但从语义看,这是一个灾难性的错误指令。 - 歧义消除的武断性:用户说“播放周杰伦的晴天”,但用户曲库里有原版、Live版、翻唱版。一个
play_music的Schema可能只要求song_name和singer。LLM输出了{"song_name": "晴天", "singer": "周杰伦"},格式正确。但具体播放哪个版本?LLM的“随机”选择可能不是用户想要的。这种语义上的不精确,在音乐、视频、文档检索等场景非常普遍。 - 安全边界的逾越:这是最危险的一类。用户提出一个不合理请求:“把我账户余额转到这个陌生账号。”一个设计不良的转账Action Schema,可能只校验
amount是数字、to_account是字符串。LLM如果缺乏足够的护栏,可能会生成一个格式完全正确但语义上为“欺诈转账”的指令。Schema在这里毫无防御能力。
应对策略:对于关键参数,绝不能依赖LLM的猜测。必须在Schema层面或前置流程中,将“必填”但“缺失”的情况,定义为一个明确的、安全的特殊Action,例如{"action": "clarify", "missing_field": "order_id", "question": "请问您要取消哪个订单?"}。同时,必须有一个独立的“安全策略层”,在JSON生成后、执行前,对高危操作(如转账、删除、权限变更)进行二次确认或基于规则的拦截。
3. 超越JSON Schema:构建语义可靠性的四层防御体系
认识到问题后,我们不能抛弃Schema,因为它提供了不可或缺的结构化基础。正确的做法是在Schema之上,构建一个多层级的“语义可靠性”防御体系。我将其归纳为以下四层,从设计时到运行时全覆盖。
3.1 第一层:增强型Schema设计——从“格式描述”到“意图描述”
首先,在Schema设计阶段就要注入语义意识。
- 富文本
description字段:不要只写“order_id: string”。要写成“order_id: string. Must be a valid order ID from the current user‘s recent orders. If ambiguous, ask for clarification.”把业务规则和异常处理期望直接写进去,作为LLM的提示词一部分。 - 引入逻辑分组与依赖关系:使用
oneOf、allOf、if-then-else等JSON Schema高级特性(如果LLM支持的话)来表达条件字段。例如,if action is refund, then the reason field is required。 - 定义“澄清”与“回退”Action:在Action集合中,明确设计如
clarify、disambiguate、not_supported等语义动作。当LLM意识到输入模糊或自身无法可靠完成时,鼓励它输出这些动作,而不是硬猜一个可能错误的业务动作。
{ "definitions": { "actions": { "oneOf": [ {"$ref": "#/definitions/business_action"}, {"$ref": "#/definitions/clarify_action"}, {"$ref": "#/definitions/fallback_action"} ] }, "clarify_action": { "type": "object", "properties": { "action": {"const": "clarify"}, "target_field": {"type": "string"}, "question": {"type": "string"} }, "required": ["action", "target_field", "question"] } } }3.2 第二层:上下文管理与状态跟踪——维持对话的“记忆线”
为Agent配备一个外部“工作记忆”,这是解决长上下文漂移的关键。
- 维护对话状态机:这个状态机不同于LLM的内部隐藏状态,而是一个结构化的、开发者定义的对象。它跟踪当前对话的“意图”(Intent)和填槽(Slot Filling)进度。例如,
{intent: “book_flight”, slots: {from: null, to: “Shanghai”, date: “tomorrow”, time: null}, confirmed: false}。 - 输出决策参考状态:在让LLM生成最终Action前,将当前的对话状态作为系统提示词的一部分输入。例如:“当前用户正在预订航班,已确定目的地为上海,时间为明天,但出发地和具体时间未定。请根据用户最新请求,决定是继续询问未填槽位,还是生成预订动作。”
- 状态更新策略:定义清晰的规则,说明LLM的输出如何更新这个外部状态。是覆盖、合并,还是触发状态重置?这能保证多轮对话的语义一致性。
3.3 第三层:运行时验证与安全护栏——执行前的最后哨兵
在Agent输出JSON之后、交给执行器(Executor)之前,插入一个强规则的验证层。
- 语法验证:使用标准JSON Schema验证器。这是基础,必须通过。
- 业务规则验证:编写独立的验证函数或规则引擎。检查内容:
- 存在性验证:提供的
order_id是否真的存在于数据库?coupon_code是否有效且未过期? - 一致性验证:
refund_amount是否不大于订单total_price?ship_to地址是否在服务范围内? - 状态机验证:尝试执行的
cancel动作,是否符合订单当前的生命周期状态?
- 存在性验证:提供的
- 安全策略验证:针对高危操作,检查是否有权限、是否超出限额、是否符合风控模型。这一步甚至可以调用独立的风控服务。
- 验证结果处理:如果验证失败,不应直接向用户抛出一个技术错误。最好能生成一个友好的澄清或错误Action,并回馈给对话流,让LLM有机会基于这个反馈进行下一轮回复。例如,验证层发现订单已发货,无法取消,它可以生成一个结构化信息
{validation_failed: true, reason: “Order already shipped, cannot cancel.”},然后由专门的处理器将其转化为面向用户的自然语言回复,或者触发一个notifyAction。
3.4 第四层:持续评估与迭代基准——定义属于你的“语义Benchmark”
格式正确性有jsonschema这样的标准工具衡量,语义正确性则需要我们自己定义评估基准(Benchmark)。这是确保Agent持续改进的闭环。
- 构建场景测试集:不要只用简单的、格式完美的查询测试。要专门构造包含以下元素的测试用例:
- 模糊用例:指令不完整、有歧义。
- 边界用例:涉及业务规则边界(如退款时限最后一天)。
- 对抗用例:诱导Agent犯错的指令。
- 多轮对话流:测试状态跟踪能力。
- 定义语义评估指标:除了“格式通过率”,更要定义:
- 意图识别准确率:Agent选择的Action,是否符合用户的真实意图?
- 槽位填充准确率:Action中的参数值,是否正确且完整?
- 业务规则遵守率:输出的Action,是否100%符合所有后台业务规则?(通过运行时验证层模拟判断)
- 安全违规率:在对抗测试中,产生危险指令的比例。
- 实施自动化回归测试:将上述测试集和评估指标集成到CI/CD流程中。每次对Agent的Prompt、Schema或相关模块进行修改后,自动运行测试,监控核心语义指标的变化,防止性能回退。
4. 实战:为一个客服工单分类Agent注入语义可靠性
理论说再多,不如看一个简化版的实战例子。假设我们要构建一个内部客服工单自动分类Agent,用户用自然语言描述问题,Agent需要输出一个符合Schema的分类JSON,以便路由给相应团队。
初始的“脆弱”Schema可能是这样的:
{ "type": "object", "properties": { "category": {"type": "string", "enum": ["登录问题", "支付失败", "商品投诉", "功能建议", "其他"]}, "urgency": {"type": "string", "enum": ["低", "中", "高"]}, "summary": {"type": "string"} }, "required": ["category", "urgency"] }这个Schema会有什么语义问题?用户说“我付不了钱,急死了!”,Agent可能输出{“category”: “支付失败”, “urgency”: “高”, “summary”: “用户支付失败很着急”}。格式完美。但语义上,“付不了钱”可能是网络问题、余额不足、银行卡限额、支付系统bug等。直接归类为“支付失败”可能不够精确,导致工单在支付团队内部仍需二次流转。
我们应用四层体系进行改造:
增强Schema设计:
{ "definitions": { "action": { "oneOf": [ { "type": "object", "properties": { "action": {"const": "classify"}, "category": {"type": "string", "enum": ["登录/认证", "支付/交易", "商品/订单", "功能/体验", "投诉/举报", "咨询/建议"]}, "sub_category": {"type": "string"}, "urgency": {"type": "string", "enum": ["低", "中", "高", "紧急"]}, "tags": {"type": "array", "items": {"type": "string"}} }, "required": ["action", "category", "urgency"] }, { "type": "object", "properties": { "action": {"const": "clarify"}, "missing_info": {"type": "string"}, "suggested_questions": {"type": "array", "items": {"type": "string"}} }, "required": ["action", "missing_info"] } ] } } }我们增加了
sub_category和tags用于更细粒度分类,并引入了clarify动作。设计对话状态:状态机跟踪
{current_ticket: {description: “”, suspected_category: null, clarified_questions: []}, needs_clarification: false}。编写运行时验证规则:
- 规则1:如果
category是“支付/交易”,但用户描述中未提及任何支付工具(如“支付宝”、“信用卡”),则触发needs_clarification,建议提问“请问您使用的是哪种支付方式?” - 规则2:如果
urgency为“紧急”或“高”,但问题描述少于10个字,则要求补充详情。 - 规则3:检查
sub_category是否与category匹配(例如,category是“商品/订单”,sub_category不应是“密码重置”)。
- 规则1:如果
构建测试集与评估:
- 测试用例:“付不了钱”(模糊) -> 期望输出:
clarify动作,询问支付方式。 - 测试用例:“刚买的手机屏幕是碎的,你们这质量太差了!今天不解决我就投诉!”(高紧急度商品投诉)-> 期望输出:
classify动作,category: “商品/订单”,sub_category: “质量问题”,urgency: “紧急”,tags: [“破损”, “投诉威胁”]。 - 评估指标:除了分类准确率,增加“模糊请求澄清率”(对于模糊输入,成功触发clarify的比例)和“紧急工单信息完备率”。
- 测试用例:“付不了钱”(模糊) -> 期望输出:
通过这套组合拳,Agent从一个只会填格式的“文书”,变成了一个懂得追问、懂得结合规则判断的“初级客服”,其输出的JSON不仅在格式上正确,在语义上也更能贴合真实业务处理的需求。
5. 工具链与架构选型思考
实现上述四层体系,不需要从头造轮子,可以结合现有生态。
- Schema与验证层:JSON Schema仍然是定义契约的基石。对于运行时验证,可以考虑Cerberus、Pydantic(Python)或Joi(JavaScript)等库,它们支持更丰富的逻辑验证。对于复杂的业务规则,可以集成一个轻量级规则引擎,如Drools或Easy Rules。
- 状态管理:对于简单场景,一个内存中的字典或Redis缓存即可。对于复杂的多轮对话管理,可以参考Rasa、Dialogflow等对话系统框架中“对话状态跟踪”的理念,或使用专门的库。
- 评估基准:可以基于pytest或unittest构建测试框架。评估指标的计算可以自动化。更高级的,可以搭建一个标注平台,持续收集人工对Agent输出的语义合理性评分,用于优化Prompt和模型。
- Agent框架选择:LangChain、LlamaIndex等主流框架提供了Agent的抽象和工具调用能力,但它们更多关注“如何让LLM使用工具”,对于本文强调的“如何让LLM可靠、安全地使用工具”,即语义可靠性,提供的原生支持有限。我们需要在其基础上,自行构建前文所述的增强Schema、状态跟踪和验证护栏层。新兴的框架如Microsoft Autogen、CrewAI在多Agent协作方面有设计,但单Agent的语义可靠性问题仍需开发者重点关注。
最终,一个可靠的LLM Ordering Agent,其核心输出可能仍然是一个JSON,但这个JSON的生成过程,已经被一个由增强设计、状态管理、运行时验证和持续评估构成的“语义保障系统”所包裹。JSON Schema是它的骨骼,而语义可靠性是它的灵魂。忽略后者,我们构建的将只是一个精美的、却可能随时做出荒唐决定的“傀儡”。在LLM应用落地的深水区,解决语义层面的可靠性,远比追求格式上的完美更为关键和迫切。这要求我们从Prompt Engineer,转变为更深思熟虑的Agent系统架构师。