# ChatModel 使用函数调用的技巧与流程

# 01. bind()与bind_tools()

前文中,我们了解过在 OpenAI 的 GPT 模型如何添加工具的描述参数,即在实例化的时候传递多一个 tools 参数,如下:

completion = client.chat.completions.create(
  model="gpt-3.5-turbo-16k",
  messages=messages,
  tools=tools,
  tool_choice="auto"
)
1
2
3
4
5
6

该流程有没有很熟悉,其实除了上面的这些参数,我们还可以传递 temperature 温度参数,这些参数本质上都是模型生成内容时传递的 运行时参数 ,所以在 LangChain 中,我们可以使用 convert_to_openai_tool 将自定义工具转换成符合 GPT 模型的参数格式,使用 .bind() 函数来传递对应的 tools 和 tool_choice ,从而完成对大语言模型函数的绑定。

运行流程如下:

图片描述

不过由于上述的步骤太过于常见,所以 LangChain 团队单独对支持 函数调用 的大语言模型添加了 .bind_tools() 函数,可以快捷地完成此操作,例如 GPT 模型的 .bind_tools() 函数的核心代码如下:

def bind_tools(
    self,
    tools: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]],
    *,
    tool_choice: Optional[
        Union[dict, str, Literal["auto", "none", "required", "any"], bool]
    ] = None,
    **kwargs: Any,
) -> Runnable[LanguageModelInput, BaseMessage]:

    formatted_tools = [convert_to_openai_tool(tool) for tool in tools]
    if tool_choice:
        if isinstance(tool_choice, str):
            
            if tool_choice not in ("auto", "none", "any", "required"):
                tool_choice = {
                    "type": "function",
                    "function": {"name": tool_choice},
                }
            
            
            if tool_choice == "any":
                tool_choice = "required"
        elif isinstance(tool_choice, bool):
            tool_choice = "required"
        elif isinstance(tool_choice, dict):
            tool_names = [
                formatted_tool["function"]["name"]
                for formatted_tool in formatted_tools
            ]
            if not any(
                tool_name == tool_choice["function"]["name"]
                for tool_name in tool_names
            ):
                raise ValueError(
                    f"Tool choice {tool_choice} was specified, but the only "
                    f"provided tools were {tool_names}."
                )
        else:
            raise ValueError(
                f"Unrecognized tool_choice type. Expected str, bool or dict. "
                f"Received: {tool_choice}"
            )
        kwargs["tool_choice"] = tool_choice
    return super().bind(tools=formatted_tools, **kwargs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 02. LLM绑定天气预报与谷歌实时搜索

例如要将前两节课实现的 天气预报 与 谷歌实时搜索 接入到 GPT 模型中,其实只需要创建好自定义工具后,将自定义工具组装成列表,然后调用 .bind_tools() 函数即可完成 LLM 对函数的绑定,当大语言模型返回的内容携带 函数调用参数 时,可以通过 .tool_calls 属性来获取对应的信息。

例如:将 GPT 模型绑定 天气预报 与 谷歌实时 ,并且当执行工具调用时,将 工具结果 附加到历史消息列表中,再次传递给大语言模型,让其生成对应的内容,完整代码如下:

import json
import os
from typing import Type, Any

import dotenv
import requests
from langchain_community.tools import GoogleSerperRun
from langchain_community.utilities import GoogleSerperAPIWrapper
from langchain_core.messages import ToolMessage
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()


class GaodeWeatherArgsSchema(BaseModel):
    city: str = Field(description="需要查询天气预报的目标城市,例如:广州")


class GoogleSerperArgsSchema(BaseModel):
    query: 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":
                if 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')}天气预报信息失败"



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


prompt = ChatPromptTemplate.from_messages([
    ("system", "你是由OpenAI开发的聊天机器人,可以帮助用户回答问题,必要时刻请调用工具帮助用户解答"),
    ("human", "{query}"),
])


llm = ChatOpenAI(model="gpt-3.5-turbo-16k", temperature=0)
llm_with_tool = llm.bind_tools(tools=tools)


chain = {"query": RunnablePassthrough()} | prompt | llm_with_tool


query = "广州现在天气怎样,有什么适合穿的衣服呢"
resp = chain.invoke(query)
tool_calls = resp.tool_calls


if len(tool_calls) <= 0:
    print("生成内容:", resp.content)
else:
    
    messages = prompt.invoke(query).to_messages()
    messages.append(resp)

    
    for tool_call in tool_calls:
        tool = tool_dict.get(tool_call.get("name"))
        print("正在执行工具: ", tool.name)
        id = tool_call.get("id")
        content = tool.invoke(tool_call.get("args"))
        print("工具输出: ", content)
        messages.append(ToolMessage(
            content=content,
            tool_call_id=id,
        ))
    print("输出内容: ", llm.invoke(messages))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130

输出内容:

正在执行工具:  gaode_weather
工具输出:  {"status": "1", "count": "1", "info": "OK", "infocode": "10000", "forecasts": [{"city": "\u5e7f\u5dde\u5e02", "adcode": "440100", "province": "\u5e7f\u4e1c", "reporttime": "2024-08-12 18:30:27", "casts": [{"date": "2024-08-12", "week": "1", "dayweather": "\u4e2d\u96e8", "nightweather": "\u4e2d\u96e8", "daytemp": "34", "nighttemp": "25", "daywind": "\u5317", "nightwind": "\u5317", "daypower": "1-3", "nightpower": "1-3", "daytemp_float": "34.0", "nighttemp_float": "25.0"}, {"date": "2024-08-13", "week": "2", "dayweather": "\u4e2d\u96e8", "nightweather": "\u4e2d\u96e8", "daytemp": "33", "nighttemp": "26", "daywind": "\u5317", "nightwind": "\u5317", "daypower": "1-3", "nightpower": "1-3", "daytemp_float": "33.0", "nighttemp_float": "26.0"}, {"date": "2024-08-14", "week": "3", "dayweather": "\u4e2d\u96e8", "nightweather": "\u5927\u96e8", "daytemp": "33", "nighttemp": "25", "daywind": "\u5317", "nightwind": "\u5317", "daypower": "1-3", "nightpower": "1-3", "daytemp_float": "33.0", "nighttemp_float": "25.0"}, {"date": "2024-08-15", "week": "4", "dayweather": "\u5927\u96e8", "nightweather": "\u5927\u96e8", "daytemp": "32", "nighttemp": "25", "daywind": "\u5317", "nightwind": "\u5317", "daypower": "1-3", "nightpower": "1-3", "daytemp_float": "32.0", "nighttemp_float": "25.0"}]}]}
输出内容:  content='广州目前的天气情况是中雨,白天气温为34摄氏度,夜间气温为25摄氏度。根据天气情况,建议您穿着轻薄透气的衣物,同时带上一把雨伞以备不时之需。请注意保持身体健康,避免长时间暴露在雨中。' response_metadata={'token_usage': {'completion_tokens': 121, 'prompt_tokens': 665, 'total_tokens': 786}, 'model_name': 'gpt-3.5-turbo-16k-0613', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None} id='run-0403f348-e59d-4ca7-9ad1-951a585d2010-0' usage_metadata={'input_tokens': 665, 'output_tokens': 121, 'total_tokens': 786}
1
2
3

掌握到这里,其实各位同学已经在有意无意之间构建出了第一个用程序实现的 Agent ,这个 Agent 拥有 实时信息搜索 和 天气预报查询 功能,但是有没有发现一个问题,原本我们使用 LECL 表达式构建的链应用是非常优雅的,但是加入了 判断 、 循环 、 工具调用 等模块后,维护起来也相对吃力,不过这个问题很快就会解决,在 LangChain 中针对 Agent 应用的创建,目前有两种封装好的策略,一种使用传统 Agent,另外一种使用 LangGraph。

# 最新版 LangChain 用法提示

  • 新版 LangChain 更推荐用 LCEL、Runnable、ChatModel.bind_tools()、结构化输出和 LangGraph 来组织复杂链路;老式 Chain、部分 AgentExecutor 写法可以读懂,但新项目应优先选择更清晰的图或 Runnable 编排。

  • 工具调用相关代码要区分两层:模型是否原生支持 tool/function calling,以及业务侧如何定义工具 schema、参数校验、错误兜底和观测日志。

  • 如果示例中的导入路径和你当前安装版本不同,优先查当前版本包内导出位置;常见迁移方向是从 langchain 拆到 langchain-core、langchain-community、langchain-openai、langgraph 等包。

# 拓展

  • 工具或插件不要只看能不能调通,更要看是否可观测、可限流、可重试、可审计。联网类工具还要处理超时、空结果、搜索噪声和结果时效性。

  • Agent 场景里,Prompt 只是调度策略的一部分;工具描述、参数 schema、历史状态、错误反馈、停止条件和人工介入点同样会影响最终稳定性。

# 常见问题

  • 为什么模型没有调用工具?常见原因是工具描述不清晰、参数 schema 过宽或过窄、用户问题不需要工具、模型本身不支持工具调用,或者工具绑定位置不对。

  • 为什么工具调用后回答仍然不准?先看工具返回是否正确,再看工具结果是否被放回模型上下文,最后检查输出解析、历史消息和异常兜底是否覆盖了真实错误。

# 面试题

  • 解释函数调用、工具调用和 Agent 的区别。

  • LangChain 中 tool schema 的作用是什么?为什么参数校验对生产环境很重要?

  • ReACT Agent 和 tool-calling Agent 的核心差异是什么?分别适合什么场景?

  • LangGraph 相比 LCEL 更适合解决哪些复杂编排问题?

# 生产问题排查

问题 常见原因 处理方式
工具没有被调用 工具描述弱、绑定失败、模型不支持 打印绑定后的模型配置,补充工具描述,换用支持工具调用的模型
参数格式错误 schema 设计不清晰,模型生成字段不稳定 使用 Pydantic/JSON Schema 校验,失败后把错误反馈给模型重试
联网结果不可用 搜索为空、接口超时、命中低质量页面 增加超时、重试、结果过滤、来源白名单和降级回答
Agent 循环不停止 缺少终止条件或工具返回被误判 设置最大迭代次数,记录每轮 thought/action/observation,增加停止规则
线上难以复现 缺少输入、工具请求和模型响应日志 给每次调用加 trace id,记录工具入参、出参、耗时和异常