邓立国《LangGraph开发AI Agent实践》1~7章试读_langgraph开发ai agent实践在线阅读-CSDN博客
在LangGraph中处理智能体的工具调用与输出解析,是实现多智能体工作流中“行动-反馈”闭环的关键环节。
8.4.1 技术详解
本小节介绍智能体的工具调用与输出的关键技术点。
1. 工具定义与注册
- 规范遵循:工具需遵循LangChain的BaseTool接口,定义清晰的name、description以及args_schema(通常为Pydantic模型)。
- 注册方式:将工具实例传入LLM的tools参数(如OpenAI的function_calling)或通过LangGraph的状态节点显式绑定。
- 目的:确保LLM能理解工具的功能、输入格式及使用场景。
2. 工具调用解析
- 输出结构识别:当LLM启用工具调用能力(如OpenAI的tool_calls字段)时,其响应中会包含结构化调用信息。
- 中间层解析:在LangGraph节点中提取tool_calls,转换为可执行的函数调用列表。
- 关键代码逻辑:检查消息是否包含tool_calls,若包含,则进入“工具执行”节点而非直接回复。
3. 输出格式化
- 结构化封装:工具执行后返回的结果(如API响应、数据库查询结果)需封装为统一格式(如字典或Pydantic模型)。
- 上下文注入:将结构化结果附加到LangGraph的状态(State)中,供后续节点使用。
- 示例:将JSON响应转换为带output和metadata的标准结构,便于追踪与调试。
4. 错误处理
- 异常捕获:在工具执行节点中使用try-except捕获参数错误、网络失败等异常。
- 状态标记:在Graph State中记录错误信息(如error: "Invalid parameter"),并可触发重试或降级逻辑。
- LLM重规划:将错误信息反馈给LLM,使其重新生成调用或调整策略。
5. 结果整合
- 自然语言生成:将工具执行结果作为上下文,由LLM节点生成面向用户的自然语言回复。
- 上下文拼接:在调用链中,将工具输出与用户原始问题、历史对话共同作为prompt输入。
- 多工具聚合:若一次调用多个工具,则需在整合节点中汇总结果,再交由LLM生成综合回答。
8.4.2 实战案例:智能工具调用与输出解析
【示例8.4】该案例是基于LangGraph的智能工具调用工作流,核心目标是让LLM自动分析用户指令、选择并调用对应工具、解析工具输出并返回自然语言结果。
# Intelligent_tool_invocation_and_output_parsing.py
# -*- coding: utf-8 -*-
import os
import json
from dotenv import load_dotenv
from typing import TypedDict, Annotated, Sequence, Dict, Any
import operator
from langgraph.graph import StateGraph, END
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.tools import BaseTool
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.exceptions import OutputParserException
from langchain_community.llms import Tongyi
# 加载环境变量
load_dotenv()
# 1. 定义复杂工具(添加Pydantic 2.x类型注解)
class WeatherTool(BaseTool):
"""天气查询工具"""
name: str = "WeatherTool"
description: str = "查询指定城市的天气信息,输入为城市名称,输出为包含温度、天气状况、风力的JSON数据"
def _run(self, city: str) -> str:
"""执行天气查询(模拟)"""
mock_weather = {
"北京": {"temperature": "5℃", "condition": "晴", "wind": "3级"},
"上海": {"temperature": "8℃", "condition": "多云", "wind": "2级"},
"广州": {"temperature": "15℃", "condition": "小雨", "wind": "4级"},
"深圳": {"temperature": "16℃", "condition": "阴", "wind": "3级"}
}
weather_data = mock_weather.get(city, {"error": f"未找到{city}的天气信息"})
return json.dumps(weather_data, ensure_ascii=False)
class TranslationTool(BaseTool):
"""翻译工具"""
name: str = "TranslationTool"
description: str = "翻译文本,输入为包含text和target_language的JSON字符串,输出为翻译结果"
def _run(self, input_json: str) -> str:
"""执行翻译(模拟)"""
try:
input_data = json.loads(input_json)
text = input_data.get("text", "")
target_lang = input_data.get("target_language", "en")
mock_translations = {
"你好": {"en": "Hello", "fr": "Bonjour", "ja": "こんにちは"},
"谢谢": {"en": "Thank you", "fr": "Merci", "ja": "ありがとう"},
"再见": {"en": "Goodbye", "fr": "Au revoir", "ja": "さようなら"}
}
result = {
"original_text": text,
"target_language": target_lang,
"translated_text": mock_translations.get(text, {}).get(target_lang, "无法翻译")
}
return json.dumps(result, ensure_ascii=False)
except json.JSONDecodeError:
return json.dumps({"error": "输入格式错误,需要JSON字符串"})
# 2. 定义状态结构
class ToolCallState(TypedDict):
messages: Annotated[Sequence[HumanMessage | AIMessage], operator.add]
user_query: str
tool_name: str = ""
tool_input: str = ""
tool_output: str = ""
parsed_output: Dict[str, Any] = {}
final_response: str = ""
# 3. 工具调用解析器(修复Prompt转义问题)
class ToolCallParser:
"""工具调用解析器"""
def __init__(self):
# 初始化通义千问LLM(兼容不同版本参数)
dashscope_api_key = os.getenv("DASHSCOPE_API_KEY")
self.llm = Tongyi(
model_name="qwen-turbo",
dashscope_api_key=dashscope_api_key,
temperature=0.0
)
# 关键修复:用双大括号转义JSON中的{},避免被误判为变量
self.prompt = ChatPromptTemplate.from_messages([
("system", """你必须严格按照以下要求输出,只输出JSON字符串,不要有任何多余内容!
你需要分析用户查询,决定调用哪个工具,并提取工具输入参数。
输出格式必须是JSON,包含以下字段:
- tool_name: 工具名称(WeatherTool或TranslationTool)
- tool_input: 工具输入参数
示例1:
用户:查询北京的天气
输出:{{"tool_name": "WeatherTool", "tool_input": "北京"}}
示例2:
用户:把"你好"翻译成法语
输出:{{"tool_name": "TranslationTool", "tool_input": "{{\\"text\\": \\"你好\\", \\"target_language\\": \\"fr\\"}}"}}
"""),
("user", "{query}")
])
self.parser = JsonOutputParser()
def parse_tool_call(self, query: str) -> Dict[str, str]:
"""解析用户查询,提取工具调用信息(带基础容错)"""
chain = self.prompt | self.llm
try:
# 获取LLM输出
llm_output = chain.invoke({"query": query})
# 清理输出(移除可能的代码块标记、多余空格)
clean_output = llm_output.strip().strip("```").strip("json").strip()
# 解析JSON
parsed = self.parser.parse(clean_output)
return {
"tool_name": parsed.get("tool_name", ""),
"tool_input": parsed.get("tool_input", "")
}
except (OutputParserException, json.JSONDecodeError) as e:
return {"tool_name": "", "tool_input": "", "error": f"解析失败:{str(e)}"}
# 4. 输出解析器
class OutputParser:
"""工具输出解析器"""
def __init__(self):
dashscope_api_key = os.getenv("DASHSCOPE_API_KEY")
self.llm = Tongyi(
model_name="qwen-turbo",
dashscope_api_key=dashscope_api_key,
temperature=0.1
)
def parse_tool_output(self, tool_name: str, tool_output: str) -> Dict[str, Any]:
"""解析工具输出"""
try:
parsed_data = json.loads(tool_output)
if tool_name == "WeatherTool":
return self._parse_weather_output(parsed_data)
elif tool_name == "TranslationTool":
return self._parse_translation_output(parsed_data)
else:
return {"raw_data": parsed_data, "type": "unknown"}
except json.JSONDecodeError:
return {"error": "工具输出格式错误", "raw_output": tool_output}
def _parse_weather_output(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""解析天气工具输出"""
return {
"type": "weather",
"temperature": data.get("temperature", "未知"),
"condition": data.get("condition", "未知"),
"wind": data.get("wind", "未知"),
"error": data.get("error", "")
}
def _parse_translation_output(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""解析翻译工具输出"""
return {
"type": "translation",
"original": data.get("original_text", ""),
"target_lang": data.get("target_language", ""),
"translated": data.get("translated_text", ""),
"error": data.get("error", "")
}
def format_response(self, parsed_output: Dict[str, Any]) -> str:
"""格式化解析结果为自然语言"""
if parsed_output.get("error"):
return f"执行失败:{parsed_output['error']}"
if parsed_output["type"] == "weather":
return f"天气信息:温度{parsed_output['temperature']},{parsed_output['condition']},风力{parsed_output['wind']}"
elif parsed_output["type"] == "translation":
return f"翻译结果:{parsed_output['original']} → {parsed_output['translated']}({parsed_output['target_lang']})"
else:
return f"执行结果:{parsed_output}"
# 5. 定义LangGraph节点
def tool_call_parser_node(state: ToolCallState) -> ToolCallState:
"""工具调用解析节点"""
parser = ToolCallParser()
parsed = parser.parse_tool_call(state["user_query"])
return {
"tool_name": parsed.get("tool_name", ""),
"tool_input": parsed.get("tool_input", ""),
"messages": state["messages"] + [AIMessage(content=f"解析结果:调用{parsed.get('tool_name', '无')},输入:{parsed.get('tool_input', '无')}")]
}
def tool_execution_node(state: ToolCallState) -> ToolCallState:
"""工具执行节点(兼容run/invoke方法)"""
tools = {
"WeatherTool": WeatherTool(),
"TranslationTool": TranslationTool()
}
tool_name = state["tool_name"]
tool_input = state["tool_input"]
if tool_name in tools:
try:
# 优先用新版本invoke,失败则回退到run
tool_output = tools[tool_name].invoke(tool_input)
except AttributeError:
tool_output = tools[tool_name].run(tool_input)
except Exception as e:
tool_output = json.dumps({"error": f"工具执行失败:{str(e)}"})
else:
tool_output = json.dumps({"error": f"未知工具:{tool_name}"})
return {
"tool_output": tool_output,
"messages": state["messages"] + [AIMessage(content=f"工具输出:{tool_output}")]
}
def output_parser_node(state: ToolCallState) -> ToolCallState:
"""输出解析节点"""
parser = OutputParser()
parsed_output = parser.parse_tool_output(state["tool_name"], state["tool_output"])
final_response = parser.format_response(parsed_output)
return {
"parsed_output": parsed_output,
"final_response": final_response,
"messages": state["messages"] + [AIMessage(content=f"解析后结果:{parsed_output}\n最终回复:{final_response}")]
}
# 6. 构建工具调用工作流
def build_tool_call_graph():
"""构建工具调用与输出解析工作流"""
graph_builder = StateGraph(ToolCallState)
# 添加节点
graph_builder.add_node("tool_call_parser", tool_call_parser_node)
graph_builder.add_node("tool_execution", tool_execution_node)
graph_builder.add_node("output_parser", output_parser_node)
# 定义工作流
graph_builder.set_entry_point("tool_call_parser")
graph_builder.add_edge("tool_call_parser", "tool_execution")
graph_builder.add_edge("tool_execution", "output_parser")
graph_builder.add_edge("output_parser", END)
return graph_builder.compile()
# 7. 测试工具调用与输出解析
if __name__ == "__main__":
# 检查API密钥配置
if not os.getenv("DASHSCOPE_API_KEY"):
print("⚠️ 警告:未配置DASHSCOPE_API_KEY环境变量,请在.env文件中添加!")
exit(1)
# 构建并运行工作流
tool_graph = build_tool_call_graph()
# 测试用例1:天气查询
print("=== 测试1:天气查询 ===")
result1 = tool_graph.invoke({
"user_query": "查询上海的天气",
"messages": []
})
print(f"工具名称:{result1['tool_name']}")
print(f"工具输入:{result1['tool_input']}")
print(f"工具输出:{result1['tool_output']}")
print(f"解析结果:{result1['parsed_output']}")
print(f"最终回复:{result1['final_response']}\n")
# 测试用例2:翻译
print("=== 测试2:翻译 ===")
result2 = tool_graph.invoke({
"user_query": "把'谢谢'翻译成日语",
"messages": []
})
print(f"工具名称:{result2['tool_name']}")
print(f"工具输入:{result2['tool_input']}")
print(f"工具输出:{result2['tool_output']}")
print(f"解析结果:{result2['parsed_output']}")
print(f"最终回复:{result2['final_response']}")
运行输出:
=== 测试1:天气查询 ===
工具名称:WeatherTool
工具输入:上海
工具输出:{"temperature": "8℃", "condition": "多云", "wind": "2级"}
解析结果:{'type': 'weather', 'temperature': '8℃', 'condition': '多云', 'wind': '2级', 'error': ''}
最终回复:天气信息:温度8℃,多云,风力2级
=== 测试2:翻译 ===
工具名称:TranslationTool
工具输入:{"text": "谢谢", "target_language": "ja"}
工具输出:{"original_text": "谢谢", "target_language": "ja", "translated_text": "ありがとう"}
解析结果:{'type': 'translation', 'original': '谢谢', 'target_lang': 'ja', 'translated': 'ありがとう', 'error': ''}
最终回复:翻译结果:谢谢 → ありがとう(ja)

8.4.3 案例代码解析
该案例代码是基于LangGraph构建的端到端智能工具调用工作流,核心逻辑是将用户自然语言指令转换为工具调用操作,再将工具输出转换为自然语言结果,整体分为“工具封装→指令解析→工具执行→结果处理→工作流编排”5个核心环节,采用线性工作流结构保证逻辑清晰。
1. 核心模块解析
1)工具封装模块
(1)基于BaseTool封装WeatherTool和TranslationTool,统一工具接口规范。
(2)每个工具通过name/description定义元信息(供 LLM 识别),_run方法实现核心业务逻辑。
(3)工具输入输出均采用JSON格式,保证结构化数据交互,降低解析复杂度。
2)指令解析模块
(1)借助通义千问LLM分析用户查询,通过Prompt强制约束输出“工具名称+输入参数”的JSON格式。
(2)对LLM输出进行清理(移除代码块标记、多余空格)和异常捕获,解决格式不规范导致的解析失败问题。
(3)核心逻辑:将非结构化的用户指令转换为结构化的工具调用参数。
3)状态管理模块
(1)定义ToolCallState类型字典,作为工作流的数据总线,统一管理。
- 输入类数据:user_query(用户查询)。
- 过程类数据:tool_name/tool_input/tool_output(工具调用信息)。
- 输出类数据:parsed_output/final_response(解析结果与最终回复)。
- 日志类数据:messages(消息记录)。
(2)采用Annotated + operator.add保证消息列表可累加,符合LangGraph状态更新规则。
4)工作流节点模块
(1)拆分为3个单一职责节点,遵循“高内聚、低耦合”原则。
- 解析节点:仅负责指令到工具参数的转换。
- 执行节点:仅负责调用工具并返回原始输出,兼容invoke/run两种调用方式。
- 输出节点:仅负责工具输出的解析和自然语言格式化。
(2)每个节点接收/返回统一的ToolCallState,保证数据流转一致性。
5)工作流编排模块
(1)用StateGraph构建线性执行流程:解析节点→执行节点→输出节点→结束。
(2)编译生成可执行的Graph对象,通过invoke方法触发完整工作流,实现“一次调用完成全流程”。
(3)无须复杂分支逻辑,聚焦工具调用的核心链路,降低新手理解成本。
2. 关键技术点解析
(1)Prompt转义:通过双大括号{{}}避免JSON格式被误判为Prompt变量,解决KeyError问题。
(2)版本兼容:适配Pydantic 2.x类型注解要求、LangChain工具调用方法的版本差异。
(3)容错设计:对JSON解析、工具调用、LLM输出等环节添加异常捕获,保证工作流不中断。
(4)结构化处理:全程采用JSON作为数据交换格式,兼顾工具调用的规范性和解析的便捷性。


1911

被折叠的 条评论
为什么被折叠?



