Skip to content

函数调用快速提取结构化数据使用技巧 ​

01.结构化输出策略与选择 ​

让LLM返回结构化输出对下游程序至关重要,下游程序通常需要特定格式的参数,目前主流的LLM结构化输出策略有3种:

  1. Prompt:通过提示词让LLM输出特定结构内容,兼容所有LLM,但输出不稳定;
  2. 函数/工具调用:为LLM绑定函数并设置强制调用模式,通过函数调用获取结构化输出,稳定性高;
  3. JSON模式:针对支持JSON输出的LLM,直接设置输出模式为JSON,稳定性高但支持模型少。

其中后两种策略稳定性更优,LangChain为其封装了**.with_structured_output()** 方法,这是获取结构化输出最简单、可靠的方式,底层会自动调用LLM原生的函数调用或JSON模式。

.with_structured_output() 核心特性 ​

  1. 接收BaseModel子类作为输入,该子类需定义输出属性的名称、描述、类型;
  2. 返回Runnable可运行对象,输出为与模型对应的Pydantic对象/字典,而非字符串或AI消息;
  3. 可通过method参数指定使用函数调用(默认) 或JSON模式;
  4. 可通过LangChain源码/高级功能列表检测LLM是否支持该方法:https://imooc-langchain.shortvar.com/docs/integcations/chat

基础使用示例(函数调用模式) ​

python
import dotenv
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI

dotenv.load_dotenv()

# 定义结构化输出的Pydantic模型
class QAExtra(BaseModel):
    """一个问答键值对工具,传递对应的假设性问题+答案"""
    question: str = Field(description="假设性问题")
    answer: str = Field(description="假设性问题对应的答案")

# 初始化模型并绑定结构化输出
llm = ChatOpenAI(model="gpt-3.5-turbo-16k")
structured_llm = llm.with_structured_output(QAExtra)

# 构建Prompt和链
prompt = ChatPromptTemplate.from_messages([
    ("system", "请从用户传递的query中提取出假设性的问题+答案。"),
    ("human", "{query}")
])
chain = {"query": RunnablePassthrough()} | prompt | structured_llm

# 调用链
print(chain.invoke("我叫慕小课,我喜欢打篮球,游泳"))

输出:question='你喜欢打篮球吗?' answer='是的,我喜欢打篮球。'

JSON模式使用示例 ​

python
import dotenv
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI

dotenv.load_dotenv()

class QAExtra(BaseModel):
    """一个问答键值对工具,传递对应的假设性问题+答案"""
    question: str = Field(description="假设性问题")
    answer: str = Field(description="假设性问题对应的答案")

# 初始化支持JSON模式的模型,指定method="json_mode"
llm = ChatOpenAI(model="gpt-4o")
structured_llm = llm.with_structured_output(QAExtra, method="json_mode")

# Prompt需明确告知JSON输出结构
prompt = ChatPromptTemplate.from_messages([
    ("system", "请从用户传递的query中提取出假设性的问题+答案,响应格式为JSON,并携带question和answer两个字段。"),
    ("human", "{query}")
])
chain = {"query": RunnablePassthrough()} | prompt | structured_llm

print(chain.invoke("我叫慕小课,我喜欢打篮球,游泳。"))

输出:question='你叫什么名字?你喜欢做什么?' answer='我叫慕小课,我喜欢打篮球,游泳。'

JSON模式注意点 ​

  1. 仅部分LLM支持JSON模式(如GPT-4o),需查阅模型官方文档确认;
  2. Prompt中必须明确JSON输出结构,否则会报错;
  3. 支持JSON模式的LLM较少,且Prompt易受干扰,优先使用函数调用模式更稳定。

关键提示 ​

创建BaseModel子类时,类名、文档字符串、参数名/描述至关重要,因为底层基于函数调用实现,这些信息是LLM绑定函数、生成正确结果的基础。

02..with_structured_output() 源码解析 ​

该方法底层根据method参数选择函数调用或JSON模式,完成模型参数绑定和输出解析,核心代码逻辑如下:

python
def with_structured_output(
    self,
    schema: Optional[_DictOrPydanticClass] = None,
    method: Literal["function_calling", "json_mode"] = "function_calling",
    include_raw: bool = False,
    **kwargs: Any,
) -> Runnable[LanguageModelInput, _DictOrPydantic]:
    if kwargs:
        raise ValueError(f"Received unsupported arguments {kwargs}")
    is_pydantic_schema = _is_pydantic_class(schema)

    # 函数调用模式
    if method == "function_calling":
        if schema is None:
            raise ValueError("schema must be specified when method is 'function_calling'. Received None.")
        llm = self.bind_tools([schema], tool_choice="any")
        # 选择输出解析器(Pydantic/JSON)
        if is_pydantic_schema:
            output_parser: OutputParserLike = PydanticToolsParser(tools=[schema], first_tool_only=True)
        else:
            key_name = convert_to_openai_tool(schema)["function"]["name"]
            output_parser = JsonOutputKeyToolsParser(key_name=key_name, first_tool_only=True)

    # JSON模式
    elif method == "json_mode":
        llm = self.bind(response_format={"type": "json_object"})
        output_parser = (
            PydanticOutputParser(pydantic_object=schema)
            if is_pydantic_schema
            else JsonOutputParser()
        )
    else:
        raise ValueError(f"unrecognized method argument. Expected one of 'function_calling' or 'json_mode'. Received: '{method}'")

    # 包含原始输出的处理逻辑
    if include_raw:
        parser_assign = RunnablePassthrough.assign(
            parsed=itemgetter("raw") | output_parser,
            parsing_error=lambda _: None
        )
        parser_none = RunnablePassthrough.assign(parsed=lambda _: None)
        parser_with_fallback = parser_assign.with_fallbacks([parser_none], exception_key="parsing_error")
        return RunnableMap(raw=llm) | parser_with_fallback
    else:
        return llm | output_parser

核心设计思路 ​

即使不使用LangChain,也可借鉴该思路实现结构化输出:构建虚假函数→让LLM绑定函数→设置tool_choice="any"强制调用,最大程度保证LLM稳定输出结构化数据。

函数调用出错捕获提升程序健壮性 ​

函数调用过程中易出现LLM参数生成错误、工具内部异常等问题,若不做错误处理,程序会异常脆弱,可通过异常捕获、回退、重试三种策略提升健壮性。

01.try/except捕获工具错误 ​

问题示例:参数缺失引发的校验错误 ​

python
import dotenv
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

dotenv.load_dotenv()

# 定义含多参数的复杂工具
@tool
def complex_tool(int_arg: int, float_arg: float, dict_arg: dict) -> int:
    """使用复杂工具进行复杂计算操作"""
    return int_arg * float_arg

# 模型绑定工具并构建链
llm = ChatOpenAI(model="gpt-3.5-turbo-16k", temperature=0)
llm_with_tools = llm.bind_tools([complex_tool])
chain = llm_with_tools | (lambda msg: msg.tool_calls[0]["args"]) | complex_tool

# 调用链(LLM仅生成前2个参数,缺失dict_arg)
print(chain.invoke("使用复杂工具,对应参数为5和2.1"))

报错:pydantic.v1.error_wrappers.ValidationError: 1 validation error for complex_tool schema - dict_arg field required

解决方案:try/except捕获异常并返回提示 ​

python
from langchain_core.runnables import RunnableConfig

def try_except_tool(tool_args: dict, config: RunnableConfig) -> Any:
    try:
        return complex_tool.invoke(tool_args, config)
    except Exception as e:
        return f"调用工具时使用以下参数:\n\n{tool_args}\n\n引发了以下错误:\n\n{type(e)}:{e}"

# 重构链
chain = llm_with_tools | (lambda msg: msg.tool_calls[0]["args"]) | try_except_tool
print(chain.invoke("使用复杂工具,对应参数为5和2.1"))

输出:

调用工具时使用以下参数:
{'int_arg':5,'float_arg': 2.1}
引发了以下错误:
<class 'pydantic.v1.error_wrappers.Validationerror'>: 1 validation error for complex_toolschema dict_arg field required (type=value_error.missing)

最优实践 ​

将错误信息重新返回给LLM,LLM会基于错误信息补全参数并重新调用工具,这是最推荐的错误处理方案,核心是在工具调用层捕获异常,独立返回错误信息。

02.回退与重试处理 ​

利用LangChain的Runnable组件特性,为函数调用添加模型回退和错误重试策略,解决LLM参数生成错误的问题。

策略1:模型回退(低性能模型→高性能模型) ​

当基础模型生成参数出错时,回退到更强大的模型(如GPT-3.5→GPT-4o)重新生成,示例:

python
import dotenv
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

dotenv.load_dotenv()

@tool
def complex_tool(int_arg: int, float_arg: float, dict_arg: dict) -> int:
    """使用复杂工具进行复杂计算操作"""
    return int_arg * float_arg

# 基础模型+高性能模型
llm = ChatOpenAI(model="gpt-3.5-turbo-16k").bind_tools([complex_tool])
better_llm = ChatOpenAI(model="gpt-4o").bind_tools([complex_tool], tool_choice="complex_tool")

# 构建基础链和回退链
better_chain = better_llm | (lambda msg: msg.tool_calls[0]["args"]) | complex_tool
base_chain = llm | (lambda msg: msg.tool_calls[0]["args"]) | complex_tool
# 为基础链添加回退策略
chain = base_chain.with_fallbacks([better_chain])

# 调用链
print(chain.invoke("使用复杂工具,对应参数为5和2.1"))

输出:10.5(GPT-4o补全dict_arg={},工具正常执行)

策略2:携带错误信息的自我重试 ​

抛出异常时,将错误信息+历史调用记录传递给LLM,让其基于错误自我纠正并重新生成参数,示例:

python
from typing import Any
import dotenv
from langchain_core.messages import ToolCall, AIMessage, ToolMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableConfig
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

dotenv.load_dotenv()

# 自定义工具异常
class CustomToolException(Exception):
    """自定义的工具错误异常"""
    def __init__(self, tool_call: ToolCall, exception: Exception) -> None:
        super().__init__()
        self.tool_call = tool_call
        self.exception = exception

@tool
def complex_tool(int_arg: int, float_arg: float, dict_arg: dict) -> int:
    """使用复杂工具进行复杂计算操作"""
    return int_arg * float_arg

# 捕获异常并抛出自定义异常
def tool_custom_exception(msg: AIMessage, config: RunnableConfig) -> Any:
    try:
        return complex_tool.invoke(msg.tool_calls[0]["args"], config)
    except Exception as e:
        raise CustomToolException(msg.tool_calls[0], e)

# 异常信息转消息,传递给LLM
def exception_to_messages(inputs: dict) -> dict:
    exception = inputs.pop("exception")
    messages = [
        AIMessage(content="", tool_calls=[exception.tool_call]),
        ToolMessage(tool_call_id=exception.tool_call["id"], content=str(exception.exception)),
        HumanMessage(content="最后一次工具调用引发了异常,请尝试使用正确的参数再次调用该工具,不要重复犯错。")
    ]
    inputs["last_output"] = messages
    return inputs

# 构建带异常占位符的Prompt
prompt = ChatPromptTemplate.from_messages([
    ("human", "{query}"),
    ("placeholder", "{last_output}"),
])

# 初始化模型并构建链
llm = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools=[complex_tool])
chain = prompt | llm | tool_custom_exception
# 添加重试回退策略
self_correcting_chain = chain.with_fallbacks(
    [exception_to_messages | chain],
    exception_key="exception",
)

# 调用重试链
print(self_correcting_chain.invoke({"query": "使用复杂工具,对应参数为5和2.1"}))

输出:10.5

注意点 ​

  1. 回退策略并非万能,若高性能模型仍生成错误参数,需检查工具描述和Prompt编写是否清晰;
  2. 重试策略的核心是将错误信息结构化传递给LLM,利用其自然语言理解能力实现自我纠正。

多模态LLM执行函数调用的技巧 ​

多模态LLM(如GPT-4o)支持文本、图像、音频等多类型输入,且原生支持函数调用,使用时只需按正常方式绑定工具,并按多模态模型规则传递输入即可,同时可通过函数调用实现多模态输出(如文生图)。

01.GPT-4o多模态输入+函数调用 ​

GPT-4o支持图片URL作为输入,结合函数调用可实现图片分析+工具调用的复合能力,例如识别图片城市→查询该城市天气。

GPT-4o原生多模态输入示例 ​

python
from openai import OpenAI
client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "what's in this image?"},
                {
                    "type": "image_url",
                    "image_url": {
                        "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/cfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
                    },
                },
            ],
        }
    ],
    max_tokens=300,
)
print(response.choices[0])

LangChain中多模态Prompt构建 ​

将文本和图片URL封装为列表格式传入Prompt,与普通Prompt使用方式一致:

python
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("human", [
        {"type": "text", "text": "{query}"},
        {"type": "image_url", "image_url": {"url": "{image_url}"}}
    ])
])

# 调用Prompt
print(prompt.invoke({
    "query": "这张图片所在的地址的天气怎么样",
    "image_url": "https://img1.baidu.com/it/u=644490943,1781886584&fm=253&fmt=auto&app=138&f=JPEG"
}).to_messages())

实战:图片识别+高德天气查询 ​

python
import json
import os
from typing import Type, Any
import dotenv
import requests
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import Field, BaseModel
from langchain_core.runnables import RunnablePassthrough
from langchain_core.tools import BaseTool
from langchain_openai import ChatOpenAI

dotenv.load_dotenv()

# 定义高德天气工具(BaseTool子类)
class GaodeWeatherArgsSchema(BaseModel):
    city: str = Field(description="需要查询天气预报的目标城市,例如:广州")

class GaodeWeatherTool(BaseTool):
    """根据传入的城市名查询天气"""
    name = "gaode_weather"
    description = "当你想询问天气或与天气相关的问题时的工具。"
    args_schema: Type[BaseModel] = GaodeWeatherArgsSchema

    def _run(self, *args: Any, **kwargs: Any) -> str:
        """运行工具获取对应城市的天气预报"""
        try:
            gaode_api_key = os.getenv("GAODE_API_KEY")
            if not gaode_api_key:
                return f"高德开放平台API秘钥未配置"
            city = kwargs.get("city", "")
            session = requests.session()
            api_domain = "https://restapi.amap.com/v3"

            # 行政区域查询获取城市编码
            city_response = session.request(
                method="GET",
                url=f"{api_domain}/config/district?keywords={city}&subdistrict=0&extensions=all&key={gaode_api_key}",
                headers={"Content-Type": "application/json;charset=utf-8"},
            )
            city_response.raise_for_status()
            city_data = city_response.json()

            # 天气预报查询
            if city_data.get("info") == "OK" and len(city_data.get("districts")) > 0:
                ad_code = city_data["districts"][0]["adcode"]
                weather_response = session.request(
                    method="GET",
                    url=f"{api_domain}/weather/weatherInfo?city={ad_code}&extensions=all&key={gaode_api_key}&output=json",
                    headers={"Content-Type": "application/json; charset=utf-8"},
                )
                weather_response.raise_for_status()
                weather_data = weather_response.json()
                if weather_data.get("info") == "OK":
                    return json.dumps(weather_data)
            session.close()
            return f"获取{kwargs.get('city')}天气预报信息失败"
        except Exception as e:
            return f"获取{kwargs.get('city')}天气预报信息失败"

# 1.构建多模态Prompt
prompt = ChatPromptTemplate.from_messages([
    ("human", [
        {"type": "text", "text": "请获取下上传图片对应城市的天气信息。"},
        {"type": "image_url", "image_url": {"url": "{image_url}"}}
    ])
])

# 2.构建天气信息整理Prompt
weather_prompt = ChatPromptTemplate.from_template("""
请整理下传递的城市的天气预报信息,并以用户友好的方式输出。
<weather>
{weather}
</weather>
""")

# 3.初始化GPT-4o并绑定天气工具
llm = ChatOpenAI(model="gpt-4o")
llm_with_tools = llm.bind_tools(tools=[GaodeWeatherTool()], tool_choice="gaode_weather")

# 4.构建多模态链
chain = (
    {
        "weather": (
            {"image_url": RunnablePassthrough()}
            | prompt
            | llm_with_tools
            | (lambda msg: msg.tool_calls[0]["args"])
            | GaodeWeatherTool()
        )
    }
    | weather_prompt
    | llm
    | StrOutputParser()
)

# 调用链(传入图片URL)
print(chain.invoke("https://imooc-langchain.shortvar.com/guangzhou.jpg"))

输出:广州未来几天的天气预报(结构化、用户友好的文本)

02.GPT-4o接入DALL-E实现多模态输出 ​

通过函数调用绑定DALL-E文生图工具,让LLM将自然语言请求转换为文生图Prompt,实现自然语言→图片的多模态输出,底层是LLM函数调用+工具执行的组合。

实战:自然语言生成图片 ​

python
import dotenv
from langchain_community.tools.openai_dalle_image_generation import OpenAIDALLEImageGenerationTool
from langchain_community.utilities.dalle_image_generator import DallEAPIWrapper
from langchain_openai import ChatOpenAI

dotenv.load_dotenv()

# 初始化DALL-E工具
dalle = OpenAIDALLEImageGenerationTool(
    name="openai_dalle",
    api_wrapper=DallEAPIWrapper(model="dall-e-3")
)

# 初始化GPT-4o并绑定文生图工具
llm = ChatOpenAI(model="gpt-4o")
llm_with_tools = llm.bind_tools(tools=[dalle], tool_choice="openai_dalle")

# 构建链
chain = llm_with_tools | (lambda msg: msg.tool_calls[0]["args"]) | dalle

# 调用链(自然语言请求)
print(chain.invoke("帮我绘制一幅老爷爷的图片"))

输出:DALL-E生成的图片URL

基于ReACT架构的Agent智能体设计与实现 ​

01.Agent概念与介绍 ​

在LLM应用中,若工具使用顺序/次数由用户输入动态决定,普通LCEL链无法满足需求,而Agent智能体可让LLM自主决定工具的使用顺序、次数和时机,核心是解决复杂、多步骤、需外部数据的任务。

Agent核心定义 ​

LangChain中的Agent是利用LLM+工具执行复杂任务的系统,设计目的是处理LLM无法直接解决的问题,其基础工作流程包含5个核心步骤:

  1. 输入理解:解析用户输入,理解意图和需求;
  2. 计划定制:基于输入制定执行计划,确定工具和执行顺序;
  3. 工具调用:按计划调用工具,执行具体操作;
  4. 结果整合:收集工具结果,解析并整合为中间输出;
  5. 反馈循环:若任务未完成,迭代上述步骤直至满足条件。

Agent核心组成模块 ​

  1. Tools:Agent可访问的工具集,每个工具执行特定功能;
  2. Executor:执行Agent计划的核心逻辑(如AgentExecutor);
  3. Prompt Templates:指导Agent理解输入、选择工具的提示词模板,可定制化。

LangChain传统Agent类型(LangChain v0.2.0) ​

LangChain封装了8种传统Agent,适配不同类型的模型,核心分类如下:

Agent类型模型类型适用场景核心要求
工具调用Chat使用支持工具调用的模型传入tools参数
OpenAI工具Chat遗留:最新OpenAI模型传入tools参数
OpenAI函数Chat旧版:OpenAI/兼容函数调用模型传入functions参数
XMLLLMAnthropic/擅长XML的模型无特殊参数
结构化聊天Chat需要传递多个工具时无特殊参数
JSON聊天Chat擅长JSON的模型无特殊参数
ReACTLLM简单模型无特殊参数
带搜索的自我问答LLM简单模型+单一搜索工具无特殊参数

文档链接:https://python.langchain.com/v0.1/docs/modules/agents/agent_types/

02.ReACT智能体运行流程与实现 ​

ReACT(Reason+Action,推理+行动)是LangChain最早支持的Agent架构,目前绝大部分Agent架构均基于其衍生,由Yao等人在2022年提出(论文:https://arxiv.org/pdf/2210.03629)。

ReACT核心思想 ​

让LLM通过自然语言推理明确下一步行动,再调用工具执行,并将工具结果作为观察反馈给LLM,迭代直至生成最终答案,核心格式为:Thought→Action→Action Input→Observation。

ReACT智能体实现步骤 ​

  1. 定义工具集;
  2. 加载ReACT专用Prompt(LangChain Hub提供);
  3. 初始化LLM;
  4. 通过create_react_agent创建ReACT Agent;
  5. 通过AgentExecutor封装执行逻辑。

实战:谷歌搜索ReACT智能体 ​

python
import dotenv
from langchain import hub
from langchain.agents import create_react_agent, AgentExecutor
from langchain_community.tools import GoogleSerperRun
from langchain_community.utilities import GoogleSerperAPIWrapper
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_core.tools import render_text_description_and_args
from langchain_openai import ChatOpenAI

dotenv.load_dotenv()

# 1.定义谷歌搜索工具的参数模型和工具
class GoogleSerperArgsSchema(BaseModel):
    query: str = Field(description="执行谷歌搜索的查询语句")

google_serper = GoogleSerperRun(
    name="google_serper",
    description=(
        "一个低成本的谷歌搜索API。"
        "当你需要回答有关时事的问题时,可以调用该工具。"
        "该工具的输入是搜索查询语句。"
    ),
    args_schema=GoogleSerperArgsSchema,
    api_wrapper=GoogleSerperAPIWrapper(),
)
tools = [google_serper]

# 2.加载ReACT官方Prompt
prompt = hub.pull("hwchase17/react")

# 3.初始化LLM
llm = ChatOpenAI(model="gpt-3.5-turbo-16k")

# 4.创建ReACT Agent
agent = create_react_agent(
    llm=llm,
    tools=tools,
    prompt=prompt,
    tools_renderer=render_text_description_and_args,
)

# 5.创建Agent执行器
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# 调用Agent
print(agent_executor.invoke({"input": "马拉松的最新世界记录是多少?"}))

典型输出流程 ​

> Entering new AgentExecutor chain...
Thought: I should use the google_serper tool to search for the latest world record in marathon.
Action: google_serper
Action Input: {'query':'latest world record in marathon'}
Observation: Kenyan athlete Kelvin Kiptum set a men's world record time of 2:00:35 on october 8,2023,at the 2023 Chicago Marathon...
Thought: I now know the final answer
Final Answer: The latest world record in marathon is 2:00:35.
> Finished chain.
{'input':'马拉松的最新世界记录是多少?','output':'The latest world record in marathon is 2:00:35.'}

03.ReACT智能体的缺陷 ​

ReACT架构的核心是通过解析LLM自然语言输出中的关键词(Thought/Action/Final Answer) 决定下一步操作,因LLM输出的不稳定性,存在诸多缺陷:

缺陷1:LLM输出格式不规范导致程序报错 ​

当提问无需调用工具的问题(如“你好,你是?”),ReACT要求LLM输出:

Thought: I now know the final answer
Final Answer: 我是一个人工智能助手,旨在帮助回答问题和提供信息。

但LLM可能直接输出:我是一个人工智能助手,旨在帮助回答问题和提供信息。,此时ReACT解析不到Final Answer关键词,直接抛出错误。

缺陷2:Prompt国际化改造成本高 ​

若将ReACT Prompt改为中文,需同步修改输出解析器(底层仍默认提取英文关键词Thought/Action),否则程序100%报错,定制化成本高。

缺陷3:对LLM能力依赖度高 ​

低性能LLM易出现推理逻辑混乱、行动选择错误、输入参数缺失等问题,导致Agent执行流程中断。

基于工具调用的智能体设计与实现 ​

为解决ReACT架构的稳定性问题,基于ChatModel+原生工具调用的Agent架构应运而生,核心是将ReACT的自然语言推理+关键词解析替换为结构化工具调用参数+模型原生解析,大幅提升稳定性。

01.工具调用智能体核心思想 ​

与ReACT架构的核心差异 ​

维度ReACT架构工具调用架构
推理输出形式自然语言文本结构化工具调用参数
下一步解析方式关键词匹配(如Thought)模型原生解析tool_calls参数
稳定性低(依赖LLM输出规范)高(结构化参数无歧义)
多工具调用支持差原生支持多工具并行调用
Prompt定制成本高(需同步修改解析器)低(无需修改解析器)

核心设计思路 ​

将ReACT的Prompt内工具描述+智能体草稿迁移为ChatModel的消息列表+工具绑定,通过检测LLM输出是普通文本还是tool_calls参数决定下一步操作:

  1. 若输出tool_calls→调用对应工具,将结果作为ToolMessage加入消息列表;
  2. 若输出普通文本→直接作为最终答案返回。

关键优势 ​

  1. 利用ChatModel原生工具调用能力,结构化输出无歧义,稳定性大幅提升;
  2. 支持多工具并行调用,执行效率更高;
  3. Prompt定制化简单,无需修改输出解析器;
  4. 兼容所有支持工具调用的ChatModel(如GPT-3.5/4o、文心一言等)。

官方文档:https://python.langchain.com/0.1/docs/modules/agents/agent_types/tool_calling/ Prompt链接:https://smith.langchain.com/hub/hwchase17/openai-tools-agent

02.工具调用智能体实现示例 ​

实现一个支持谷歌实时搜索+DALL-E文生图的工具调用Agent,LLM可根据用户输入自主选择工具,步骤与ReACT Agent基本一致,核心差异为使用create_tool_calling_agent创建Agent。

python
import dotenv
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_community.tools import GoogleSerperRun
from langchain_community.tools.openai_dalle_image_generation import OpenAIDALLEImageGenerationTool
from langchain_community.utilities import GoogleSerperAPIWrapper
from langchain_community.utilities.dalle_image_generator import DallEAPIWrapper
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_openai import ChatOpenAI

dotenv.load_dotenv()

# 1.定义工具参数模型和工具集
## 谷歌搜索工具
class GoogleSerperArgsSchema(BaseModel):
    query: str = Field(description="执行谷歌搜索的查询语句")

google_serper = GoogleSerperRun(
    name="google_serper",
    description=(
        "一个低成本的谷歌搜索API。"
        "当你需要回答有关时事的问题时,可以调用该工具。"
        "该工具的输入是搜索查询语句。"
    ),
    args_schema=GoogleSerperArgsSchema,
    api_wrapper=GoogleSerperAPIWrapper(),
)

## DALL-E文生图工具
class DallEArgsSchema(BaseModel):
    query: str = Field(description="输入应该是生成图像的文本提示(prompt)")

dalle = OpenAIDALLEImageGenerationTool(
    name="openai_dalle",
    api_wrapper=DallEAPIWrapper(model="dall-e-3"),
    args_schema=DallEArgsSchema,
)

tools = [google_serper, dalle]

# 2.构建工具调用Agent的Prompt
prompt = ChatPromptTemplate.from_messages([
    ("placeholder", "{chat_history}"),
    ("system", "You are a helpful assistant"),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

# 3.初始化支持工具调用的LLM
llm = ChatOpenAI(model="gpt-4o-mini")

# 4.创建工具调用Agent
agent = create_tool_calling_agent(
    prompt=prompt,
    llm=llm,
    tools=tools,
)

# 5.创建Agent执行器
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# 调用Agent(文生图请求)
print(agent_executor.invoke({"input": "请帮我绘制一张老爷爷爬山的图片"}))

# 调用Agent(搜索请求)
print(agent_executor.invoke({"input": "马拉松的世界记录是多少?"}))

文生图请求输出 ​

> Entering new AgentExecutor chain...
Invoking: 'openai_dalle' with '{'query':'An elderly man climbing a mountain, with a determined expression...'}'
https://dalleproduse.blob.core.windows.net/.../generated_00.png
> Finished chain.
{'input':'请帮我绘制一张老爷爷爬山的图片','output':'这是我为您绘制的老爷爷爬山的图片:[图片URL]'}

搜索请求输出 ​

> Entering new AgentExecutor chain...
Invoking: 'google_serper' with '{'query':'马拉松世界纪录2023'}'
2023年10月芝加哥马拉松,基普图姆以2小时00分35秒打破世界纪录...
> Finished chain.
{'input':'马拉松的世界记录是多少?','output':'截至2023年10月,马拉松的世界纪录是2小时00分35秒,由基普图姆在芝加哥马拉松创造。'}

内置的其他Agent类型介绍与上手 ​

LangChain v0.2.0之前封装了多种传统Agent,均基于推理-行动-观察核心思想,适配不同类型的LLM/ChatModel(如擅长XML/JSON的模型),使用时仅需修改Prompt+替换创建方法,其余逻辑完全复用。

01.典型Agent示例:XML Agent ​

适配擅长XML解析的模型(如Anthropic),通过<tool>/<tool_input>/<final_answer>等XML标签定义执行流程,实现步骤:

  1. 构建XML格式的Prompt;
  2. 使用create_xml_agent创建Agent;
  3. 封装AgentExecutor执行。
python
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.agents import create_xml_agent, AgentExecutor

# 1.构建XML Agent Prompt
prompt = ChatPromptTemplate.from_messages([
    ("human", """
You are a helpful assistant. Help the user answer any questions.
You have access to the following tools:
{tools}
In order to use a tool, you can use <tool></tool> and <tool_input></tool_input> tags.
You will then get back a response in the form <observation></observation>
When you are done, respond with a final answer between <final_answer></final_answer>.
Begin!
Previous Conversation:
{chat_history}
Question: {input}
{agent_scratchpad}
""")
])

# 2.初始化LLM和Agent
llm = ChatOpenAI(model="gpt-4o-mini")
agent = create_xml_agent(
    llm=llm,
    prompt=prompt,
    tools=tools,  # 复用谷歌搜索+文生图工具
)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# 调用Agent
print(agent_executor.invoke({"input": "马拉松的世界记录是多少?", "chat_history": ""}))

02.典型Agent示例:JSON Agent ​

适配擅长JSON解析的模型,要求LLM输出指定格式的JSON数据(action/action_input),核心是修改Prompt为JSON格式规范,使用create_json_agent创建Agent。

JSON Agent核心Prompt片段 ​

python
prompt = ChatPromptTemplate.from_messages([
    ("system", "Assistant is a large language model trained by OpenAI..."),
    ("placeholder", "{chat_history}"),
    ("human", """
TOOLS
Assistant can ask the user to use tools to look up information...
RESPONSE FORMAT INSTRUCTIONS
when responding to me, please output a response in one of two formats:
**Option 1 (call tool):**
```json
{{"action": "工具名", "action_input": "工具输入"}}

Option 2 (final answer):

json
{{"action": "Final Answer", "action_input": "最终答案"}}

USER'S INPUT Here is the user's input: {input} {agent_scratchpad} """) ])


## 03.所有传统Agent的异同点
### 相同点
1. 均包含**input**(原始问题)和**agent_scratchpad**(智能体草稿)两个核心输入变量;
2. 均为**单Agent自我执行**,不支持多Agent协作;
3. 均可通过**切换Prompt+create_xxx_agent()** 快速切换,无需修改大量代码;
4. 核心设计思想均为**推理-行动-观察**,仅Prompt格式不同;
5. 均使用**单一LLM**完成推理和答案生成,不支持多LLM分工;
6. 除工具调用Agent外,其余Agent**国际化改造需同步修改输出解析器**。

### 不同点
1. **Prompt风格**:ReACT为自然语言、XML Agent为XML标签、JSON Agent为JSON格式;
2. **输出解析器**:解析规则由Prompt决定,部分支持多工具调用,部分不支持;
3. **输入编码**:部分支持历史聊天记录(chat_history),部分不支持;
4. **模型适配性**:不同Agent适配不同能力的LLM/ChatModel。

# AgentExecutor源码解析与Agent组件缺陷
## 01.AgentExecutor源码解析
LangChain中所有传统Agent均需通过**AgentExecutor**封装执行,其核心作用是实现**工具调用的循环执行+结果反馈**,是Agent的“执行引擎”。

### 核心运行逻辑(伪代码)
```python
# 获取初始行动
next_action = agent.get_action(inputs)
# 循环执行,直至生成Final Answer
while next_action != AgentFinish:
    # 执行工具,获取观察结果
    observation = run(next_action)
    # 将观察结果反馈给Agent,获取下一个行动
    next_action = agent.get_action(inputs, next_action, observation)
# 返回最终答案
return next_action

核心代码:_call()函数 ​

python
def _call(
    self,
    inputs: Dict[str, str],
    run_manager: Optional[CallbackManagerForChainRun] = None,
) -> Dict[str, Any]:
    # 工具名映射,便于快速查询
    name_to_tool_map = {tool.name: tool for tool in self.tools}
    # 中间步骤:存储(AgentAction, Observation)元组
    intermediate_steps: List[Tuple[AgentAction, str]] = []
    iterations = 0  # 迭代次数
    start_time = time.time()  # 开始时间

    # 循环执行:未超过迭代次数/时间限制
    while self._should_continue(iterations, time.time() - start_time):
        # 获取下一步行动
        next_step_output = self._take_next_step(
            name_to_tool_map, inputs, intermediate_steps, run_manager=run_manager
        )
        # 若为最终答案,直接返回
        if isinstance(next_step_output, AgentFinish):
            return self._return(next_step_output, intermediate_steps, run_manager)
        # 将步骤添加到中间结果
        intermediate_steps.extend(next_step_output)
        # 检测工具是否需要直接返回结果
        if len(next_step_output) == 1:
            tool_return = self._get_tool_return(next_step_output[0])
            if tool_return is not None:
                return self._return(tool_return, intermediate_steps, run_manager)
        # 迭代次数+1
        iterations += 1

    # 超过限制,返回停止响应
    output = self.agent.return_stopped_response(self.early_stopping_method, intermediate_steps, **inputs)
    return self._return(output, intermediate_steps, run_manager)

核心运行流程 ​

  1. 初始化:构建工具映射、中间步骤存储、迭代计数器;
  2. 循环判断:检查是否超过最大迭代次数/时间限制;
  3. 获取行动:调用Agent的_take_next_step方法获取下一步行动(AgentAction);
  4. 工具执行:根据AgentAction调用对应工具,获取观察结果(Observation);
  5. 结果反馈:将Observation加入中间步骤,反馈给Agent;
  6. 终止判断:若获取到AgentFinish(最终答案)或工具需直接返回,终止循环并返回结果;
  7. 超时处理:若超过限制,返回停止响应。

02.传统Agent组件的缺陷 ​

LangChain传统Agent(基于AgentExecutor)解决了普通LCEL链无法循环执行的问题,但因过度封装+黑盒机制,存在诸多致命缺陷,无法满足复杂Agent应用的需求:

缺陷1:无条件分支,仅支持线性循环 ​

Agent执行流程为单一线性循环,无法根据输入/工具结果实现条件分支/路由(如数学问题走数学工具链,物理问题走物理工具链)。

缺陷2:不支持多Agent协作 ​

所有传统Agent均为单Agent独立执行,无法实现多Agent分工、协作、通信(如一个Agent负责搜索,一个Agent负责数据分析)。

缺陷3:Prompt与解析器过度耦合 ​

传统Agent的Prompt和输出解析器高度绑定,定制化修改成本极高,例如修改Prompt语言、调整执行逻辑,需同步修改底层解析器。

缺陷4:黑盒机制,无中间干预能力 ​

Agent和AgentExecutor均为黑盒执行,无法在执行过程中插入自定义逻辑(如手动修改工具参数、中断执行、添加人工审核)。

缺陷5:扩展能力差,难以动态切换LLM/工具 ​

传统Agent的LLM和工具在初始化时绑定,运行时无法动态切换,也难以添加记忆、知识库等扩展功能,灵活性极低。

03.解决方案:LangGraph ​

上述缺陷的核心解决方案是LangGraph(LangChain v0.2.0核心特性),它基于图结构实现Agent的执行逻辑,支持:

  1. 条件分支/路由:基于节点输出动态决定下一步执行节点;
  2. 多Agent协作:多个Agent作为独立节点,实现分工、通信;
  3. 透明化执行:所有节点和边均可定制,支持中间干预;
  4. 动态扩展:运行时可动态切换LLM、工具、添加记忆/知识库;
  5. 兼容LCEL:与LangChain的LCEL表达式无缝集成,调用统一(invoke/stream/batch)。

LangGraph是LangChain构建复杂Agent应用的核心框架,可实现串联数百个节点、上百种工具的复杂工作流。

核心知识点总结 ​

一、LLM工具调用 ​

  1. 函数调用的核心价值:获取结构化输出、弥补LLM知识时效性、对接外部工具/系统;
  2. LangChain创建自定义工具的3种方式:@tool装饰器、StructuredTool.from_function()、BaseTool子类;
  3. 工具绑定方法:bind()和bind_tools(),后者为工具调用专用快捷方法;
  4. 非原生工具调用LLM的兼容方案:Prompt指定格式+输出解析;
  5. 结构化输出最优方案:with_structured_output(),底层基于函数调用/JSON模式;
  6. 函数调用错误处理:try/except异常捕获、模型回退、携带错误信息的重试。

二、多模态LLM工具调用 ​

  1. 多模态输入:按模型规则传递文本+图片URL,LangChain中封装为列表格式;
  2. 多模态输出:通过函数调用绑定DALL-E文生图工具,实现自然语言→图片;
  3. 核心实战:图片识别+工具调用(如图片城市→天气查询)。

三、Agent智能体开发 ​

  1. Agent核心定义:LLM+工具+自主决策,解决复杂、多步骤、需外部数据的任务;
  2. ReACT架构:Reason+Action,自然语言推理+关键词解析,稳定性差;
  3. 工具调用架构:结构化工具调用参数+模型原生解析,稳定性高,为当前主流方案;
  4. 传统Agent类型:ReACT/XML/JSON/工具调用等,均基于推理-行动-观察思想;
  5. AgentExecutor:Agent的执行引擎,实现循环工具调用+结果反馈。

四、传统Agent的缺陷与解决方案 ​

  1. 核心缺陷:无分支、不支持多Agent协作、黑盒机制、扩展能力差;
  2. 解决方案:LangGraph,基于图结构实现Agent的灵活、可扩展、可干预的执行逻辑。

Talk is cheap, show me the code!