8.4 处理智能体的工具调用与输出解析《LangGraph开发AI Agent实践》

邓立国《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: AnnotatedSequence\[HumanMessage \| AIMessage, operator.add]

user_query: str

tool_name: str = ""

tool_input: str = ""

tool_output: str = ""

parsed_output: Dictstr, 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) -> Dictstr, 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) -> Dictstr, 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: Dictstr, Any) -> Dictstr, 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: Dictstr, Any) -> Dictstr, 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: Dictstr, 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 = toolstool_name.invoke(tool_input)

except AttributeError:

tool_output = toolstool_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作为数据交换格式,兼顾工具调用的规范性和解析的便捷性。

相关推荐
冬奇Lab1 小时前
开源项目第197期:skill-up — 阿里巴巴出品的 Agent Skills 评测与进化工具,评测闭环 + 自动修复
人工智能·开源·资讯
玫瑰互动GEO1 小时前
海外GEO优化案例-ChatGPT搜索关键词排名GEO优化案例详解(含RAG机制与Tokenization技术拆解)
人工智能·ai·chatgpt·geo优化
冬奇Lab1 小时前
Code Agent 解剖(10):agent 崩了怎么恢复,对话历史存在哪?
人工智能·开源·agent
IanSkunk1 小时前
视光中心建设复盘:从流程断层到组织能力的落地路径
大数据·人工智能
AI备案指南-满满1 小时前
人工智能拟人化互动服务安全自评估报告的评估要点有哪些?
人工智能·算法·安全·机器人·大模型备案·算法备案
围炉聊科技1 小时前
OpenAdapt 源码拆解:录制一次,如何实现确定性回放
人工智能
SLD_Allen1 小时前
字节跳动飞连(Feilian)AI智能体零信任安全治理深度技术研究报告
网络·人工智能·安全·智能体安全
IT_陈寒2 小时前
Redis的DEL命令居然没删干净数据?这个坑我爬了半天
前端·人工智能·后端
熊猫钓鱼>_>2 小时前
鸿蒙ArkUI全手势操作实战指南:6大基础手势从原理到落地避坑
人工智能·深度学习·华为·架构·harmonyos·arkui·tapgesture