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 = "翻译文本,输入为包含texttarget_languageJSON字符串,输出为翻译结果"

   

    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: 工具名称(WeatherToolTranslationTool

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值