Day 09 / 共 20 天 · 第 2 周 Agent 执行
输出解析 output_parser
LLM 按 schema 输出后,怎么解析成"选哪个工具 + 参数"?今天读 agent/output_parser.py:AgentGPTAction、JSON 容错、两个 parser、自我纠错。
📍 你在整门课的位置 · 第 2 周「Agent 执行」
D1-5 核心概念→
D6 执行循环→
D7 工作流→
D8 提示词(入)→
D9 输出解析(出)→
D10 LLM抽象
L01
解析的任务
🤔 Day 08 我已经在提示里让 LLM"必须按 JSON schema 输出"了,直接
json.loads 不就完了?为什么还要写一整个解析器?
因为 LLM 不是永远听话:它可能把 JSON 包在 ```json 代码块里、用 Python 的 True 而非 JSON 的 true、用单引号、带尾逗号、把参数多包一层……直接 json.loads 经常当场报错。💡 一句话本质:解析器是"不可靠文本 → 确定动作"的翻译 + 抢救层
Day 08 是"给 LLM 的输入",今天是"LLM 给回的输出",正好一进一出。解析器的活儿:假设 LLM 会犯格式错,层层清洗容错(剥代码块 → 抽 JSON → 修布尔 → 吃 Python 字面量),尽力从不完美文本里提取出
AgentGPTAction(name, args)。这层越健壮,智能体越稳——它是"不可靠 LLM"和"确定程序"之间的关键缓冲。Day 08 让 LLM 按 JSON schema 输出(thoughts + tool)。但 LLM 返回的是一段字符串(可能带代码块包裹、格式瑕疵)。output_parser 的任务:把这段字符串解析成程序能用的"动作"——即"用哪个工具、参数是什么"。
为什么需要专门的解析器?
LLM 不是永远听话——它可能:把 JSON 包在 ```` ```json ```` 里、用 Python 的 True 而非 JSON 的 true、多个逗号、字段名带引号问题……直接
📦 生活类比(贯穿今天):解析器就像公司收发室——外面寄来的快递(LLM 输出)包装五花八门:有的用胶带缠十圈、有的盒子压扁了、有的地址写歪了。收发室的活儿就是不管包装多乱,都拆开、登记成一张标准内部单据(
json.loads 经常失败。解析器负责"尽力从 LLM 的不完美输出里提取出结构化动作"——是"不可靠的 LLM 文本"到"确定的程序动作"之间的关键翻译层。这层的健壮性直接决定智能体稳不稳。📦 生活类比(贯穿今天):解析器就像公司收发室——外面寄来的快递(LLM 输出)包装五花八门:有的用胶带缠十圈、有的盒子压扁了、有的地址写歪了。收发室的活儿就是不管包装多乱,都拆开、登记成一张标准内部单据(
AgentGPTAction)再转交给对应部门(工具)。收发室越能干,公司越不会因为"一个烂包裹"卡住。L02
AgentGPTAction:动作的数据结构
# agent/output_parser.py:11
class AgentGPTAction:
name: str # 工具名
args: dict # 参数
读法:解析的产物就是一个
AgentGPTAction——"调哪个工具(name)+ 传什么参数(args)"。这个动作交给 ToolExecutor(Day 06)去执行。简单干净的数据结构。和 CrewAI 的
AgentAction(Day 07)、AutoGPT 的 tool_call 是同一个东西——都是"把 LLM 的决策结构化成'动作'对象"。天下 Agent 一个样。L03
parse 流程
AgentSchemaOutputParser.parse(output_parser.py:27,迭代步用):
# :30-31 剥掉 ``` 代码块包裹
# :32 JsonCleaner.extract_json_section 抽出 JSON 段
# :34 clean_boolean 修 true/false
# :39-44 ast.literal_eval 解析,从 response_obj['tool']['name'] 和 ['args'] 取动作
response_obj = ast.literal_eval(response)
args = response_obj['tool']['args'] if 'args' in response_obj['tool'] else {}
return AgentGPTAction(name=response_obj['tool']['name'], args=args)
📝 从 LLM 原始输出到动作(输入 → 输出)
LLM 原始返回(带代码块 + Python 风格 True):
→ 输出:
这是我的决定:
```json
{"thoughts": {"reasoning": "先把结果写文件", "speak": "正在保存"},
"tool": {"name": "Write File", "args": {"file_name": "out.txt", "overwrite": True}}}
```
→ 解析步骤:剥掉 ```json 包裹 → extract_json_section 忽略"这是我的决定:"前缀抽出 JSON 段 → clean_boolean 把 True 修成合法字面量 → ast.literal_eval 解析→ 输出:
AgentGPTAction(name="Write File", args={"file_name":"out.txt","overwrite":True}),交给 ToolExecutor 执行。🛠️ 简化版 → 真实版对照(每多一步都在兜一种 LLM 坏习惯)
朴素做法:
真实版:
action = json.loads(llm_text) # 直接解析
问题:上面例子里的输出会当场 JSONDecodeError——因为有 ```json 包裹、有前缀废话、还有 Python 的 True。真实版:
剥代码块(兜"爱用 ```json 包裹")→ extract_json_section(兜"爱加开场白")→ clean_boolean(兜"写 True 不写 true")→ ast.literal_eval(兜"用单引号/Python 字面量")。朴素版少的每一步,对应真实里 LLM 的一种常见坏习惯。读法:流程:剥代码块 → 抽 JSON → 修布尔值 →
ast.literal_eval 解析 → 从 tool.name/tool.args 取出动作。每一步都在"清洗 LLM 的不完美输出"。L04
JSON 容错
为什么用 ast.literal_eval 而非 json.loads?因为 LLM 常输出 Python 风格(True/'单引号')而非严格 JSON。clean_boolean 先把 true/false 修成 Python 的 True/False,再用 ast.literal_eval(能吃 Python 字面量)解析。
⚠️ 常见误解:小白常以为"我在提示里要求 LLM 输出 JSON 了,它返回的就一定是合法 JSON,直接
json.loads 就行"。大错——LLM 是"尽量照做",不是"保证照做",尤其弱模型经常带代码块、Python 风格 True、单引号、尾逗号。永远别信任 LLM 的输出格式,要当"脏输入"来防御。解析器假设 LLM 会犯错,用一条清洗流水线尽最大努力把"脏文本"抢救成确定的动作对象。
这些容错细节为什么重要?
严格
🎓 生活类比:
💥 具体事故:LLM 回
json.loads 遇到 True(Python 风格)、单引号、尾逗号就报错。但 LLM 就是经常这么输出(尤其弱一点的模型)。如果解析器不容错,智能体动不动就"解析失败"卡住。JsonCleaner.extract_json_section(抽出 JSON 段,忽略前后废话)、clean_boolean(修布尔)、ast.literal_eval(吃 Python 字面量)——层层容错,尽最大努力从 LLM 输出里"抢救"出结构化数据。健壮的解析器是智能体可靠性的关键(呼应 CrewAI 的 json_repair、Day 07)。🎓 生活类比:
json.loads 像机器阅卷——格式差一点(涂到框外)就判 0 分(报错);ast.literal_eval + 清洗链像宽容的老教师——认得出手写体、涂改、写歪的答案,照样把分给你。智能体要的是"尽量读懂",不是"格式警察"。💥 具体事故:LLM 回
```json {...} ```,若直接 json.loads → 抛 JSONDecodeError → 这轮动作丢失 → 智能体原地卡住(甚至白白烧掉一次 LLM 调用的钱)。容错链就是来救这条命的。L05
两个 parser
AgentSchemaOutputParser(:27,迭代步用):期待复杂 schema——顶层有thoughts+tool(tool 里再有 name/args)。AgentSchemaToolOutputParser(:50,TOOL 步用):期待扁平 schema——顶层直接是name/args,没有 tool 外壳。
为什么两个 parser?
因为两种步骤要 LLM 输出的格式不同(Day 06):迭代步要 LLM 同时输出思考 + 选工具(复杂 schema,因为 LLM 要"想"再"选");TOOL 步工具已钦定,只需 LLM 填参数(扁平 schema,简单)。解析器与提示 schema 严格配套——提示要什么格式,就用对应的 parser 解。这体现了"Parser/Schema 配套"的设计——两份提示模板(superagi.txt / agent_tool_input.txt)对应两个解析器。
L06
参数清洗
ToolExecutor.clean_tool_args(agent/tool_executor.py:67)会把 {"value": x} 这种包装拆平,容错 LLM 的参数格式。
又一层容错
LLM 有时会把参数值多包一层,比如把
{"file_name": "a.txt"} 写成 {"file_name": {"value": "a.txt"}}。clean_tool_args 把这种多余的 {"value": ...} 包装拆掉,让参数格式规整后再传给工具的 _execute。解析出动作后、执行前还有这道清洗——从"LLM 输出"到"工具能用的参数"之间,SuperAGI 做了多层清洗容错。假设 LLM 会犯格式错、并逐层兜住,是智能体健壮性的功夫。L07
自我纠错
如果 LLM 选了个不存在的工具,ToolExecutor.execute(tool_executor.py:56)不崩,而是返回一个错误提示让 LLM 下轮改正;工具执行出错则设 retry=True 让智能体重试。
"把错误变成给 LLM 的反馈"
LLM 可能选个拼错的工具名、或工具执行失败。SuperAGI 不是直接崩溃,而是把"你选的工具不存在,可用的有 X/Y/Z"或"工具报错了:...”作为反馈写进 Feed,喂给下一轮 LLM——让它自己纠正。下一轮 LLM 看到反馈,改选正确的工具或调整参数。这种"错误 → 反馈 → 自我纠正"是 Agent 健壮性的核心(eino/OpenHands/CrewAI 都有)。智能体像人一样"犯错、看反馈、改正"——这才是"自主"的真谛。
🍽️ 生活类比:就像餐厅上错菜——顾客说"我点的是牛肉面,这是鸡肉面"(反馈写进 Feed),服务员(LLM)下一轮据此重做,而不是整个餐厅关门(崩溃)。把错误变成"给下一轮的一句反馈",是自我纠错的精髓。
🍽️ 生活类比:就像餐厅上错菜——顾客说"我点的是牛肉面,这是鸡肉面"(反馈写进 Feed),服务员(LLM)下一轮据此重做,而不是整个餐厅关门(崩溃)。把错误变成"给下一轮的一句反馈",是自我纠错的精髓。
L08
今日小结 + 动手
🧠 今天你应该能回答
- output_parser 的任务是什么?为什么需要它?
- AgentGPTAction 是什么?和别的框架的"动作"什么关系?
- 为什么用 ast.literal_eval 而非 json.loads?有哪些容错?
- 两个 parser 为什么?和提示 schema 怎么配套?
- 选错工具/执行出错怎么"自我纠错"?
✋ 动手
P=superagi/agent
sed -n '11,70p' output_parser.py # AgentGPTAction + 两个 parser
sed -n '18,73p' tool_executor.py | head -40 # 未知工具/清洗/纠错
明天预告 · Day 10(第2周收官):LLM 抽象——多模型(OpenAI/Palm/Replicate/本地)统一接口、适配器抹平差异、工厂选实现、token 计数、自动重试。