# 不支持函数调用的大语言模型解决技巧

# 01. Prompt实现函数调用功能思路

由于市面上的大语言模型众多,并不是所有的大语言模型都支持 函数调用 这个功能的,还存在相当多的大模型并不支持(特别是开源模型),对于这类模型,其实也可以通过编写特定的 Prompt ,即可让模型调用适当的工具,原理其实就是让大语言模型尽可能按照特定的格式进行输出(例如输出函数的调用参数 JSON 数据)。

图片描述

所以在这种解决方案中,就需要将 函数调用 的相关参数嵌入到 prompt 中一起传递给大语言模型,并且告知大语言模型生成参数的响应格式,接下来针对大语言模型输出的内容进行解析,如果解析到参数则调用函数,否则正常输出。

该解决方案虽然可以临时为大语言模型添加 函数调用 功能,但是缺陷也非常大:

  • 大模型输出的不确定性,并不是每一次都可以正确调用函数的,应用很容易出错。

  • 如果函数参数很复杂,产生的 prompt 可能会非常长,占用大模型的上下文。

  • 没法一次性调用多个工具,或者一次性调用多个工具,会让大语言模型的输出变得异常混乱,对模型考验能力大。

在 LangChain 中,除了封装了 convert_to_openai_tool() 工具快速将工具转换成 GPT 模型的函数参数,还可以使用 render_text_description_and_args() 或者 render_text_description() 快速将工具转换成 描述文本 ,使用技巧一模一样,其中前者转换的描述带参数结构信息,后者仅为函数基础介绍。

# 02. Prompt函数调用示例

例如在上节课的 GPT绑定函数调用参数 示例中,将示例修改成通过 prompt 填充函数参数,并让大语言模型按照特定的规则生成相应的数据(可以使用任意的大语言模型来完成这个示例),完整代码如下:

import json
import os
from typing import Type, Any, TypedDict, Dict, Optional

import dotenv
import requests
from langchain_community.tools import GoogleSerperRun
from langchain_community.utilities import GoogleSerperAPIWrapper
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import Field, BaseModel
from langchain_core.runnables import RunnablePassthrough, RunnableConfig
from langchain_core.tools import BaseTool, render_text_description_and_args
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')}天气预报信息失败"


class ToolCallRequest(TypedDict):
    """工具调用请求字典"""
    name: str
    arguments: Dict[str, Any]


def invoke_tool(
        tool_call_request: ToolCallRequest, config: Optional[RunnableConfig] = None,
):
    """
    我们可以使用的执行工具调用的函数。

    :param tool_call_request: 一个包含键名和参数的字典,名称必须与现有工具的名称匹配,参数是该工具的参数。
    :param config: 这是LangChain使用的包含回调、元数据等信息的配置信息。
    :return: 请求工具的输出内容。
    """
    tool_name_to_tool = {tool.name: tool for tool in tools}
    name = tool_call_request["name"]
    requested_tool = tool_name_to_tool[name]
    return requested_tool.invoke(tool_call_request["arguments"], config=config)



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()]

system_prompt = """您是一个由OpenAI开发的聊天机器人,可以访问以下一组工具。
以下是每个工具的名称和描述:

{rendered_tools}

根据用户输入,返回要使用的工具的名称和输入。
将您的响应作为具有`name`和`arguments`键的JSON块返回。
`arguments`应该是一个字典,其中键对应于参数名称,值对应与请求的值。"""
prompt = ChatPromptTemplate.from_messages([
    ("system", system_prompt),
    ("human", "{query}"),
]).partial(rendered_tools=render_text_description_and_args(tools))

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

chain = (
        {"query": RunnablePassthrough()}
        | prompt
        | llm
        | JsonOutputParser()
        | RunnablePassthrough.assign(output=invoke_tool)
)

print(chain.invoke("马拉松世界记录是多少?"))
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
131
132
133
134
135
136
137
138

输出内容:

{'name': 'google_serper', 'arguments': {'query': '马拉松世界记录是多少'}, 'output': '2004年1月1日,国际田联宣布,马拉松开始拥有世界纪录。 2023年10月8日晚(北京时间),在芝加哥马拉松比赛中,肯尼亚人基普图姆在芝加哥马拉松比赛中以2小时00分35秒的成绩打破基普乔格保持的世界纪录。'}
1

如果该链应用没法正常调用函数也很正常,因为在链中使用了 JsonOutputParser() 输出解析器,但大语言模型并不会所有回答都输出 JSON 信息,如果某一次回复大语言模型输出了 正常字符串 ,例如提问 “你好,你是?”,这个时候链应用就崩溃了,所以在生产环境中,涉及到 函数调用 、 路由逻辑 等需要规范化数据的内容,尽可能使用支持 函数调用 的大语言模型,避免程序变得很脆弱。

# 最新版 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,记录工具入参、出参、耗时和异常