一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

从字符串约定到 LangChain:拆解 LLM 的 Tool Calling 流程

时间:2026-09-19 17:40:01 编辑:袖梨 来源:一聚教程网

当用户询问天气时,大语言模型本身并不掌握实时数据,需要先请求外部函数获取结果,再组织成自然语言回答。这个看似简单的过程涉及工具选择、参数解析、函数执行和结果回传。下面从手写字符串协议开始,逐步过渡到 LangChain 的结构化工具调用,并梳理实现中容易忽略的边界情况。

用户问“北京天气怎么样”,模型需要先获得天气数据,才能给出有依据的回答。Tool Call 就是把这个需求交给程序执行的一种机制。

在本文的本地函数示例中,模型负责选择工具和生成参数,Python 负责执行工具,再将结果交回模型。

用户:查一下 beijing 的天气
  ↓
模型请求:get_weather(city="beijing")
  ↓
Python 执行:得到 sunny,30度
  ↓
结果交回模型
  ↓
模型回答:北京晴,气温 30 度

这里的天气来自代码中写死的字典,只是演示数据。接入真实天气服务时,需要替换工具内部的查询逻辑。

项目中的三个文件,逐步展示了这套机制:

文件请求怎样产生程序怎样处理
01_toolcall.py手写字符串,模拟模型输出按冒号拆分,再调用函数
02_prompt_protocol_model.py用提示词要求模型输出标签正则提取工具名,JSON 解析参数,再调用函数
03_langchain_model.py向模型提供工具定义,接收结构化请求读取 tool_calls、执行工具、回传 ToolMessage

本文基于这些源文件讲解。代码修正以片段形式给出,原 Python 文件未修改,也未实际调用模型验证。

从第一个文件开始,先不用模型。

01_toolcall.py 定义了一个普通 Python 函数:

def get_weather(city: str) -> str:
    weather = {
        "beijing": "sunny",
        "shanghai": "cloudy",
        "fuzhou": "rainy",
    }
    return weather.get(city, "未知天气")

然后用一行字符串模拟模型输出:

model_output = "get_weather: fuzhou"

程序按约定拆出工具名和参数:

def parse_model_output(text: str) -> tuple[str, dict[str, str]]:
    tool_name, city = text.split(":", maxsplit=1)
    return tool_name.strip(), {"city": city.strip()}

tool_name, tool_args = parse_model_output(model_output)

if tool_name == "get_weather":
    result = get_weather(**tool_args)

其中,get_weather(**{"city": "fuzhou"}) 相当于 get_weather(city="fuzhou")

这段代码展示了工具调用的基本结构:解析请求 → 找到函数 → 传入参数 → 得到结果。它没有调用 LLM,也没有把结果交回模型。

这种字符串协议适合入门,但扩展起来很麻烦。没有冒号会解析失败,多个参数也需要重新设计格式。

第二个文件接入了真实模型,但工具调用格式仍由提示词约定。

02_prompt_protocol_model.py 要求模型在遇到天气问题时输出:

<Tool>get_weather</Tool>
<Args>{"city":"南昌"}</Args>

程序从普通文本中提取两个标签:

tool_match = re.search(r"<Tool>(.*?)</Tool>", text, re.DOTALL)
args_match = re.search(r"<Args>(.*?)</Args>", text, re.DOTALL)

(.*?) 捕获标签内的内容,re.DOTALL 让匹配可以跨行。提取到的参数仍然是字符串,需要再解析:

args = json.loads(args_match.group(1))

于是,模型生成的文本被转换为:

{"tool": "get_weather", "args": {"city": "南昌"}}

接下来仍由 Python 执行 get_weather(**call["args"])。当前示例到打印工具结果为止,没有再次调用模型生成自然语言回答。

这也解释了自定义协议的问题:模型可能漏写标签、输出无效 JSON,或者缺少 city。原代码遇到 JSON 解析失败时返回空字典,后续调用又可能因为缺少参数而报错。更合适的做法是明确报告格式错误,并在执行前检查参数:

args = call["args"]
if not isinstance(args, dict) or not isinstance(args.get("city"), str):
    raise ValueError("工具参数必须包含字符串类型的 city")

readme.md 中的 <Tool><Args> 应理解为本例的自定义协议,不能据此推断所有模型内部都使用这套标签。

第三个文件使用模型接口支持的结构化工具调用,并通过 LangChain 统一处理。

03_langchain_model.py 给函数添加了 @tool

from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """查询演示天气,city 使用 beijing、fuzhou 或 nanchang。"""
    weather = {
        "beijing": "sunny,30度",
        "fuzhou": "rainy,20度",
        "nanchang": "cloudy,15度",
    }
    return weather.get(city, "未知天气")

默认情况下,函数名用作工具名,文档字符串说明用途,类型注解帮助生成参数定义。装饰器将函数包装成 LangChain 工具,供后续绑定和执行。LangChain 工具定义文档

然后将工具绑定到模型:

llm = load_llm().bind_tools([get_weather])
response = llm.invoke(messages)

bind_tools() 向模型提供工具定义。调用后,可以从 response.tool_calls 读取结构化请求;执行仍由应用代码负责。底层模型和服务接口需要支持工具调用。LangChain 工具调用文档

返回值可能类似下面这样,具体 ID 由接口生成:

[
    {
        "name": "get_weather",
        "args": {"city": "beijing"},
        "id": "call_example",
        "type": "tool_call",
    }
]

这里有三个需要使用的字段:

字段用途
name找到要执行的工具
args提供已经解析好的参数
id将执行结果与原调用关联

因此,第三种方式省去了手写标签和正则解析。它仍然需要程序检查参数、处理执行错误,不能保证模型一定选对工具或填对城市。

执行工具后,还差一次结果回传。

原代码在 for tool_call in response.tool_calls 循环外创建 ToolMessage,会出现两个问题:

  • 没有工具调用时,resulttool_call 尚未赋值,却被使用。
  • 一次请求多个工具时,只回传最后一个结果,前面的调用缺少对应消息。

每个工具调用都应该得到自己的结果消息。 ToolMessagetool_call_id 必须对应原请求的 id,模型才能关联请求与结果。LangChain 结果回传说明

可以保留第三个文件中的工具定义、load_llm() 和入口代码,用下面的函数替换 main()

def main() -> None:
    user_prompt = " ".join(sys.argv[1:]).strip() or DEFAULT_USER_PROMPT
    tools = {get_weather.name: get_weather}
    llm = load_llm().bind_tools(list(tools.values()))

    messages = [
        SystemMessage(content=SYSTEM_PROMPT),
        HumanMessage(content=user_prompt),
    ]

    for _ in range(5):
        response = llm.invoke(messages)
        messages.append(response)

        if response.invalid_tool_calls:
            print("工具调用参数解析失败,请检查模型输出。")
            return

        if not response.tool_calls:
            print(response.content)
            return

        for call in response.tool_calls:
            selected_tool = tools.get(call["name"])

            try:
                if selected_tool is None:
                    raise ValueError(f"未知工具:{call['name']}")
                result = selected_tool.invoke(call["args"])
            except Exception as exc:
                result = f"工具执行失败:{exc}"

            messages.append(
                ToolMessage(
                    content=str(result),
                    name=call["name"],
                    tool_call_id=call["id"],
                )
            )

    print("已达到 5 轮调用上限,任务尚未完成。")

这个循环处理了三种情况:没有工具请求就输出回答;有请求就逐个执行、逐个回填;模型需要继续调用工具时进入下一轮。5 轮是教学示例的预算,达到上限会明确停止。模型接口本身的网络异常仍需另行处理。

对一次天气查询,常见的消息顺序是:

HumanMessage:查询北京天气
AIMessage:请求 get_weather,参数为 beijing
ToolMessage:返回 sunny,30度,关联该次调用 ID
AIMessage:根据工具结果回答用户

修正后的代码还通过工具名查表分发,避免将所有请求都直接交给 get_weather。以后添加其他工具时,可以沿用这套分发方式。

运行前,在项目目录安装所需依赖:

cd D:workspaceysh_aibackendpythontoolcall
python -m venv .venv
..venvScriptspython.exe -m pip install langchain-openai langchain-core python-dotenv

第二、第三个文件都从脚本所在目录加载 .env,对应路径是 src/.env

API_KEY=你的密钥
BASE_URL=服务接口地址
MODEL=该服务支持的模型名称

这里的变量名是项目约定。第三个示例所选模型必须支持工具调用,具体可用模型以服务提供方为准。

原代码还有几处值得在运行前处理:

  • 第二个文件会打印完整 API Key,删除该调试输出。
  • 第二个文件的 f-string 内外复用了双引号。为兼容 Python 3.10、3.11,将内层改为单引号:return f"{city} 的天气是: {weather.get(city, '未知天气')}"
  • 第二个文件只打印了“模型原始输出”的标题,没有打印正文。取得 model_output 后补上 print(model_output),才能观察标签或普通文本回答。
  • 第三个文件用 "".join(sys.argv[1:]) 拼接参数,会丢失单词间空格。上面的修正版使用 " ".join(...)

然后按顺序运行:

..venvScriptspython.exe src1_toolcall.py
..venvScriptspython.exe src2_prompt_protocol_model.py "帮我查一下南昌的天气"
..venvScriptspython.exe src3_langchain_model.py "帮我查一下 beijing 的天气"

三个文件的城市键并不完全一致。例如第三个示例使用 nanchang,传入“南昌”会查不到。可以统一参数约定,或在工具内部增加城市别名转换。

验证第三个示例时,分别输入“你好”“查询 beijing 天气”“查询 beijing 和 fuzhou 天气”,观察零次、一次和可能的多次工具请求。重点看 response.tool_calls 与结果消息是否逐一对应;模型是否在同一轮请求两个城市,取决于实际响应。

当这套流程跑通后,把字典查询换成天气 API、数据库查询或文档检索,工具调用的主流程仍然相同:接收模型请求,执行明确的操作,把结果交回模型。

热门栏目