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

目前市面上的 LLM 除了能接收文本数据,还出现了不少多模态 LLM,这些 LLM 不仅能接收文本,还能接收 图像 、 音频 等内容,并且这类 多模态LLM 也支持 函数调用 功能,要想使用这些模型调用工具,只需要按照正常的方式将工具绑定到模型上,在传递 消息 给大语言模型时,按照 多模态LLM 的特定规则即可。

# 01. GPT-4o 多模态输入

OpenAI 提供的 GPT-4o 模型是一个多模态的模型(输入多模态),在传递 消息列表 时,可以在 人类消息 中添加 图片地址 ,这样即可将特定的图片上传给 GPT-4o 模型进行识别,从而实现多模态输入。

  • GPT-4o 多模态输入文档链接: https://platform.openai.com/docs/api-reference/chat/create

官方提供原生示例如下:

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/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
                    },
                },
            ],
        }
    ],
    max_tokens=300,
)

print(response.choices[0])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

在 LangChain 中,如果消息也是多模态的,只需要按照特定的模型对应的 消息结构 历史创建 LangChain消息 即可,例如在上面的示例中, 人类消息 传递了一个列表,包含了 文本 和 图片链接 ,转换成 LangChain消息/提示模板 如下:

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

print(prompt.invoke({
    "query": "这张图片所在的地址的天气怎么样",
    "image_url": "https://img1.baidu.com/it/u=644490943,1781886584&fm=253&fmt=auto&app=138&f=JPEG"
}).to_messages())
1
2
3
4
5
6
7
8
9
10
11

使用技巧和普通的提示模板没有差异,只是将原本的 字符串 替换成了 列表字典 的格式,输出内容:

[HumanMessage(content=[{'type': 'text', 'text': '这张图片所在的地址的天气怎么样'}, {'type': 'image_url', 'image_url': {'url': 'https://img1.baidu.com/it/u=644490943,1781886584&fm=253&fmt=auto&app=138&f=JPEG'}}])]
1

这里我们以 GPT-4O + 天气预报查询工具 + 多模态输入 三个组件构建一个 LLM 应用,让该 LLM 应用能识别上传图片城市所在的天气信息,运行流程如下:

图片描述

示例代码如下:

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


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":
                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')}天气预报信息失败"



prompt = ChatPromptTemplate.from_messages([
    ("human", [
        {"type": "text", "text": "请获取下上传图片对应城市的天气信息。"},
        {"type": "image_url", "image_url": {"url": "{image_url}"}}
    ]),
])
weather_prompt = ChatPromptTemplate.from_template("""请整理下传递的城市的天气预报信息,并以用户友好的方式输出。

<weather>
{weather}
</weather>""")


llm = ChatOpenAI(model="gpt-4o")
llm_with_tools = llm.bind_tools(tools=[GaodeWeatherTool()], tool_choice="gaode_weather")


chain = (
        {
            "weather": (
                    {"image_url": RunnablePassthrough()}
                    | prompt
                    | llm_with_tools
                    | (lambda msg: msg.tool_calls[0]["args"])
                    | GaodeWeatherTool()
            )
        } | weather_prompt | llm | StrOutputParser()
)

print(chain.invoke("https://imooc-langchain.shortvar.com/guangzhou.jpg"))
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

输出内容:

以下是广州未来几天的天气预报信息:



**发布时间:2024年7月11日 12:00**


- **白天天气:** 大雨
- **夜间天气:** 大雨
- **白天温度:** 33°C
- **夜间温度:** 25°C
- **白天风向:** 西南
- **夜间风向:** 西南
- **风力等级:** 1-3级


- **白天天气:** 大雨
- **夜间天气:** 中雨-大雨
- **白天温度:** 33°C
- **夜间温度:** 24°C
- **白天风向:** 北
- **夜间风向:** 北
- **风力等级:** 1-3级


- **白天天气:** 中雨-大雨
- **夜间天气:** 中雨-大雨
- **白天温度:** 32°C
- **夜间温度:** 25°C
- **白天风向:** 西南
- **夜间风向:** 西南
- **风力等级:** 1-3级


- **白天天气:** 中雨-大雨
- **夜间天气:** 大雨
- **白天温度:** 33°C
- **夜间温度:** 25°C
- **白天风向:** 北
- **夜间风向:** 北
- **风力等级:** 1-3级

请注意天气变化,做好防雨准备。
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

# 02. GPT-4o 接入 DALL·E 文生图

除了输入的多模态,大语言模型的输出其实也可以通过曲线救国的方式实现 多模态输出 ,有使用过 ChatGPT-Plus 的小伙伴肯定了解,当我们和 ChatGPT 对话时,可以让 ChatGPT 帮忙生成对应的图片,该功能底层本质上也是通过 函数调用 来实现的,运行原理非常简单:

  • 创建一个根据 文本 生成 图片 工具,例如可以使用 DALLE,工具的调用参数为生成图片的 Prompt。

  • 将工具绑定到 LLM 上,并预设特定的 Prompt,让 LLM 将用户输入的 query 转换成绘图的 Prompt。

  • 调用工具,将 Prompt 生成图片,并将 工具消息 、 AI消息 叠加到历史消息中,再次提问,获得最终答复。

运行流程和刚刚我们创建的 图片获取天气预报应用 非常接近,如下:

图片描述

实现代码如下:

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

dalle = OpenAIDALLEImageGenerationTool(
    name="openai_dalle",
    api_wrapper=DallEAPIWrapper(model="dall-e-3")
)
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("帮我绘制一幅老爷爷的图片"))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

输出内容:

https://oaidalleapiprodscus.blob.core.windows.net/private/org-8mmokaDgFXOdOsE9HC1PNBZM/user-XbaFwYqigWuDrIeZS6l61HgI/img-NxKXd4xBMJZdIixcxYQe5dmd.png?st=2024-08-15T05%3A02%3A53Z&se=2024-08-15T07%3A02%3A53Z&sp=r&sv=2024-08-04&sr=b&rscd=inline&rsct=image/png&skoid=d505667d-d6c1-4a0a-bac7-5c84a87759f8&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2024-08-14T17%3A52%3A40Z&ske=2024-08-15T17%3A52%3A40Z&sks=b&skv=2024-08-04&sig=WHJ0%2BKw9OKRSKOaYLXL%2Btataexl5Vf9lJAGVxvmO9kM%3D
1

生成的图片示例:

图片描述

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