许多关于AI智能体的教程都推荐用同样的方法来解决输出错误的问题:使用反射机制。如果你的智能体生成的JSON数据有误,那就再调用一次大语言模型来“审核”这些数据即可。第二次调用会对比第一次生成的结果,然后第一次调用会重新尝试生成,这样一来,质量就能得到提升。这种方法看起来很简单、很优雅,也符合学术规范。
然而,我在一家大型互联网公司实际部署过智能体系统,那些系统负责生成部署配置文件、API请求数据以及数据库查询语句。根据我的惨痛经验可以告诉你们:反射机制对于结构化输出来说并不起作用。它既不可靠,也不能在真正关键的时候发挥作用。
实际情况是这样的:你的智能体生成的JSON数据有三分之一的内容是错误的,比如字段缺失、数据类型错误,或者违反了业务规则。你按照教程的建议添加了反射步骤,结果现在系统有六分之一的时间会出错。
乍一听这似乎是一种进步,但当你意识到那些仍然出现的错误其实是“被忽视”的时候,你就会明白问题的严重性了。反射机制明明显示“结果正确”,但实际上这些错误依然存在。你构建了一个总是会出错的系统,而直到在某个周六凌晨2点生产环境出现问题时,你才会发现这一点。
我花了数周时间调试这个流程,最终才找到了一种真正有效的方法。这种方法简单得令人惊讶,它能让我的系统几乎永远生成正确的结果,而且完全不需要什么复杂的反射提示机制。让我来给大家展示一下吧。
我们将涵盖的内容:
先决条件
要想充分理解这篇文章的内容,你需要掌握以下基础知识:
-
Python基础知识(函数、字典、类型提示等)
-
大语言模型API的工作原理(发送请求并获取结果)
-
什么是JSON模式(你不需要成为专家,代码本身就能说明问题)
反射机制存在的问题
在我看来,让一个大语言模型去评判另一个大语言模型生成的结构化数据,就相当于让一个数学不好的人去给另一个数学不好的人打分。他们很可能会遇到同样的问题或类似的盲点。那些导致错误产生的因素,现在又被用来检测这些错误本身——为什么它们在第二次尝试时就能正确地识别出这些问题呢?
在反思环节,你需要仔细思考自己实际上是在要求模型做什么。“嘿,看看你刚刚生成的这段JSON代码。timeout_seconds是否应该小于interval_seconds?副本数量和CPU使用限制是否符合我在系统提示中列出的业务规则?”
模型会重新阅读这些代码,通过模式匹配来判断它们是否“符合要求”,然后回答“没问题”。但在生成代码的过程中,它可能会忽略某些约束条件;在审查代码时,它同样也会犯同样的错误,因为还是同一个模型在运用相同的推理机制。
一直让我困扰的问题并不是错误的输出结果,而是那些被“认定为正确”的错误结果。也就是所谓的误报现象。反思环节会提示“这个配置是正确的”,但实际上却完全错了。
一个能够提示“我失败了,请重试”的系统虽然有点烦人,但至少是安全的;而一个在系统出现故障时还声称“一切正常”的系统呢?这样的配置很可能会通过所有的审核流程,最终导致你的服务崩溃。那可就是半夜被紧急叫醒处理问题的原因了。
对于那些开放性较强的任务来说,反思机制确实非常有用——比如帮助改进邮件的措辞、找出文章中的逻辑漏洞、或者为博客文章提供更好的结构建议。但对于那些有明确约束条件的结构化输出来说,你就需要一种不会随意猜测、能够给出确定结果的验证方法。
解决办法:确定性验证
解决这个问题的方法非常简单:
生成代码 → 用真实的验证工具进行验证 → 将具体的错误信息反馈回去 → 重新尝试。
就是这么简单。不需要再次调用大型语言模型来进行“评估”,也不需要进行复杂的逻辑推理来判断结果是否正确。只需要一个能够返回true或false以及具体错误信息的函数而已——这种验证工具其实和用于处理表单提交或API请求的验证工具是一样的。
关键在于:当你明确告诉大型语言模型哪些地方出了问题时,它们在纠正错误方面表现得非常出色;但它们自己发现错误的能力却很弱。
当你告诉模型“你的输出存在以下这些问题:timeout_seconds必须小于interval_seconds,如果副本数量大于5,则cpu_limit必须大于或等于1.0”时,它几乎每次都能在下一次尝试中纠正这些错误。
纠正错误本身其实很简单,真正困难的是发现错误。而使用这种技术,你就可以把这个任务交给一个总是能够在几微秒内准确完成验证工作的确定性函数来处理。这样就不会出现“自认为正确但实际上却是错误的”情况了,结果只会是“通过”或“失败”,并且会附带具体的错误原因。
验证工具真正能检测到什么(以及为什么大型语言模型做不到)
确定性验证工具会在三个层面上检查错误,而每一层都会利用大型语言模型所不擅长的地方来进行检测:
1. 结构性错误
生成的结果是否是有效的JSON格式?所有必需的字段是否都存在?数据类型是否正确(字符串、整数还是数组)?JSON Schema工具可以在几微秒内完成这些检查。
当一个大型语言模型“审查”相同的输出时,它可能会看一下其结构,然后认为“看起来像是有效的JSON格式”,但实际上并不会对其进行解析。而验证器则会真正地对这些数据进行解析;不存在“看起来像”这样的概念,数据要么通过验证,要么不通过。
2. 规则违反情况
replicas的值是否在1到20这个允许的范围内?service_name是否符合正则表达式^[a-z][a-z0-9-]*$?memory_limit_mb是否至少为128?
这些都属于边界值检查。众所周知,大型语言模型在进行精确的数值比较或正则表达式匹配时表现得很差,它们只能进行近似处理,而验证器则会进行严格的判断。
3. 跨字段业务规则
在这里,机器学习模型的能力就显得十分有限了。像“如果replicas大于5,那么cpu_limit必须大于或等于1.0”或者“timeout_seconds必须严格小于interval_seconds”这样的规则,需要同时考虑两个数值,并应用特定的逻辑关系来进行判断。
这些规则并不存在于训练数据中,模型也无法通过模式匹配来识别它们。它们是你们的系统所特有的规则。对于大型语言模型来说,除非这些规则被明确地包含在输入信息中,否则它根本没有理由知道这些规则的存在,而长篇的输入信息也容易使这些规则被忽略。
这就是为什么验证器在这三个方面都能胜出:它不需要进行推理——它只是执行任务而已。不存在任何解释过程、注意力窗口,也不会因为上下文中的某些内容更显重要而忽略某个规则。每条规则都会被按顺序、确定性地执行。
相比之下,大型语言模型的职责是生成某种看起来正确的结果,它是基于一定的模式来产生这些结果的。这与验证规范中是否满足了所有要求是完全不同的能力。你不会让一位小说家去校对纳税申报表,也不会让一个生成器去验证它自己产生的输出。
代码实现
以下是LangGraph中的完整代码结构:包括验证器、各个节点以及带有条件路由功能的图结构。完整的可运行示例——包括模式定义、验证器代码、循环逻辑和测试用例——可以在GitHub上找到:github.com/manishramavat/langgraph-deterministic-validation
首先,这是模式定义和验证器的代码,它们才是你们真正的依据:
from jsonschema import validate, ValidationError
DEPLOYMENT_CONFIG_SCHEMA = {
"type": "object",
"required": ["service_name", "replicas", "resources", "health_check"],
"properties": {
"service_name": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"},
"replicas": {"type": "integer", "minimum": 1, "maximum": 20},
"resources": {
"type": "object",
"required": ["cpu_limit", "memory_limit_mb"],
"properties": {
"cpu_limit": {"type": "number", "minimum": 0.1, "maximum": 8.0},
"memory_limit_mb": {"type": "integer", "minimum": 128, "maximum": 16384},
},
},
"health_check": {
"type": "object",
"required": ["path", "timeout_seconds", "interval_seconds"],
"properties": {
"path": {"type": "string", "pattern": "^/"},
"timeout_seconds": {"type": "integer", "minimum": 1},
"interval_seconds": {"type": "integer", "minimum": 5},
},
},
},
}
# 验证器:你们真正的依据。这才是难点所在。
def validate_config(config: dict) -> tuple[bool, list[str]]:
"""模式验证 + 业务规则判断。这就是你们的规范文件。”
errors = []
try:
validate(instance=config, schema=DEPLOYMENT_CONFIG_SCHEMA)
except ValidationError as e:
errors.append(f"模式错误:{e.message}(发生于路径{list(e.path)})")
return False, errors # 如果结构不合法,就直接返回错误结果
# JSON Schema无法表达的跨字段规则检查
if config["replicas"] > 5 and config["resources"]["cpu_limit"] < 1.0:
errors.append(f"当replicas大于5时,cpu_limit必须大于或等于1.0")
if config["health_check"]["timeout_seconds"] >= config["health_check"]["interval_seconds":
errors.append("timeout_seconds必须小于interval_seconds")
return len(errors) == 0, errors
现在来看将代码生成环节与验证环节连接起来的LangGraph循环结构:
import json
from typing import TypedDict
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
SYSTEM_PROMPT = ("你需要生成有效的JSON格式的部署配置文件。"
"必填字段包括:service_name、replicas、resources和health_check。"
"必须严格遵循所有规定,仅返回JSON对象。")
class State(TypedDict):
request: str
config: dict | None
errors: list[str]
attempts: int
llm = ChatOpenAI(model="gpt-4o", temperature=0.2)
def generate_node(state: State) -> dict:
"""生成配置文件,在重试时会故意加入一些错误信息。」
content = f"正在为{state['request']}生成配置文件..."
if state["errors"]: # 关键在于:会反馈具体的错误信息,而不会给出模糊的批评
content += "\n\n你之前的尝试出现了以下错误:\n"
content += "\n".join(f"- {e}" for e in state["errors"])
content += "\n请修复所有这些错误。"
resp = llm.invoke([SystemMessage(content=SYSTEM_PROMPT), HumanMessage(content=content)])
try:
config = json.loads(resp.content.strip()) if resp.content else {}
except json.JSONDecodeError:
config = None # 验证器会处理这种异常
return {"config": config, "attempts": state["attempts"] + 1}
def validate_node(state: State) -> dict:
"""进行确定性的验证,不使用任何大型语言模型。」
if not state["config"]:
return {"errors": ["输出结果不是有效的JSON格式"]}
_, errors = validate_config(state["config"])
return {"errors": errors}
def route(state: State) -> str:
"""如果验证通过或重试次数达到上限,就结束流程。"""
if not state["errors":
return "完成"
return "重试" if state["attempts"] < 3 else "完成"
graph = StateGraph(State)
graph.add_node("generate", generate_node)
graph.add_node("validate", validate_node)
graph.set_entry_point("generate")
graph.add_edge("generate", "validate")
graph.add_conditional_edges("validate", route, {"retry": "generate", "done": END})
app = graph.compile()
这个流程最终形成了一个具有确定性退出条件的循环结构:要么输出结果通过验证,要么重试次数达到3次,此时就需要采取进一步的措施。整个过程中并没有使用任何复杂的编排框架,所有的验证工作都由专门的验证工具来完成。
为什么这种方法效果如此好
你把两种本质上不同的任务分开了:错误检测和错误修正,并且将每一项任务交给了最适合执行它的工具。
验证工具在错误检测方面表现得非常出色。几十年来,我们一直有JSON模式验证器、SQL解析器以及类型检查工具。这些工具都是经过充分验证的成熟技术,它们的运行速度极快,从不会生成错误的验证结果,也不会在任何情况下出现故障。此外,它们也不会被在训练过程中遇到的复杂边缘案例所干扰。
第二项任务正是大语言模型表现不佳的地方:系统性地检查所有约束条件并非下一代字符预测所优化的目标。
当这两者结合在一起时,它们的效果几乎堪称完美。验证器能够发现所有问题(因为它的检测结果是确定性的),而大语言模型也能纠正验证器找出的所有错误(因为反馈信息非常明确)。但单独来看,它们在共同完成的这项任务中表现都不尽如人意:验证器无法生成配置文件,而大语言模型也无法可靠地验证这些配置文件。然而当它们协同工作时,其效果就会远超其中任何一方单独行动,尤其是对于这类错误而言,这种协作方式的效果要好得多。
当三次尝试仍然不够时
如果模型在三次尝试内都无法解决问题,那么第四次尝试几乎也不会有任何帮助。剩下的错误通常源于规格描述中的模糊之处,而非生成过程中的技术问题。因此,在系统设计之初就应明确“放弃”意味着什么:
-
记录失败原因,包括具体的请求内容以及最终的错误信息——这些信息能帮你找出规格描述中哪些地方存在模糊性。
-
以明确的错误代码进行拒绝(例如返回422错误码,并附上详细的验证失败信息),而不是将有问题的配置文件继续传递下去。
-
对于高风险情况,及时寻求人工干预。
无论你采取什么措施,都不要浪费资源去期待第七次尝试就能解决问题。
何时使用这种方法,何时不应使用
有一个简单的测试标准:你是否能够编写一个函数,这个函数能够根据你的代理程序的输出返回true或false?
如果可以的话,就将这个函数集成到“生成→验证→重试”的循环中。你的验证器本来就已经存在了,只是还没有将其加入代理程序的反馈机制中而已:
-
如果是JSON格式的输出,你已经有相应的schema结构了,可以直接运行
jsonschema.validate()来进行验证。 -
如果是SQL格式的输出,可以运行
EXPLAIN命令,数据库会告诉你这个查询语句是否能够被正确解析。 -
如果是代码输出,只需编译代码并运行测试即可。这些测试过程本身就构成了验证机制。
-
对于Terraform脚本,也存在专门的
terraform validate命令用于验证。
如果无法满足这个条件——也就是说,“正确”的标准具有主观性(比如电子邮件的语气、摘要的质量、宣传文案的说服力等等)——那么你就需要回到人工审核或反思的步骤中去。这也没关系,因为对于这类主观性的评估,人工审核确实是最有效的方法。不过,当存在明确的对错标准时,人工审核就无法发挥作用了。
总结
首先构建验证器,然后再开发代理程序。你的验证器本身就代表了你的规格要求,它用机器可以识别的标准定义了“正确”的含义。一旦有了这个验证机制,你的代理程序就可以成为一个具有明确退出条件的简单循环系统,这样你就能更有信心地评估它的可靠性,而不再需要依赖提示语的巧妙程度了。
不要让大语言模型自己去验证自己的输出结果,而是要为它们提供一个能够真实反映实际情况的“镜子”。
所有这些观点都仅代表我个人,并不代表我的雇主。