LangChain 1.3.18 + DeepSeek-v4-flash 工具调用踩坑实录

版本:langchain==1.3.18,langchain-core、langchain-openai 使用配套最新版本;通过 OpenAI 兼容接口对接 DeepSeek-v4-flash。

本文基于真实调试案例,从现象、代码、底层协议、高频坑点完整梳理,面向刚接触 LangChain Tool-Call 的开发者。

前言

Agent 智能体标志性落地能力是 Tool-Call(工具调用):

  • 大模型的职责:输出结构化工具调用 JSON 指令;
  • 应用层职责:解析指令、在本地执行 Python 工具函数,把执行结果封装成协议消息回传给大模型,再由大模型做后续推理生成回答。

⚠️核心认知:

大模型永远不会执行你的本地 Python 函数,它只输出调用指令;是否输出工具指令,是模型按照工具 Schema、上下文提示、tool_choice 参数做输出侧的生成决策,不是"远程调用执行"。

bind_tools

的作用:把工具函数转换成 JSON Schema,随请求传给 LLM,告诉模型"你可以输出这些工具的调用格式",

它不会控制模型一定输出工具调用,只是给模型提供可选输出模板

很多新手重大误区:被 @tool 装饰器迷惑,以为装饰完,大模型就可以远程跑本地函数。完全不是,全部工具执行逻辑都要业务代码自己处理。

下面以「获取当前系统时间」作为 Demo,完整复现从幻觉输出、工具指令、消息协议到 400 报错,直到业务跑通的全过程。

依赖安装

python 复制代码
pip install langchain==1.3.18 langchain-openai python-dotenv

.env 配置文件

python 复制代码
DEEPSEEK_API_KEY=sk-xxx
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1

完整 Demo:从幻觉输出到业务跑通

python 复制代码
import datetime
from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
import os
from langchain_core.messages import SystemMessage, HumanMessage, ToolMessage
from langchain_core.tools import tool

load_dotenv(override=True)

model = init_chat_model(
    model="deepseek-v4-flash",
    model_provider="openai",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url=os.getenv("DEEPSEEK_BASE_URL")
)

message = HumanMessage(content="查询当前时间")

# 1、原始提问,不带工具Schema下发
resp = model.invoke([message])
print(f"resp1:{resp.content}")

@tool
def get_now():
    """获取当前系统时间"""
    return f"当前时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"

# 2、bind_tools:下发工具Schema,无SystemMessage约束
# bind_tools:仅追加tools schema到请求体,不强制模型输出
model_with_tools = model.bind_tools([get_now])
resp = model_with_tools.invoke([message])
print(f"resp2:{resp.content},tools:{resp.tool_calls}")

# 3、增加SystemMessage作为行为提示约束
SYS_MSG = SystemMessage(
    content="你拥有获取当前时间工具,user查询当前时间或日期,优先输出get_now的工具调用指令,禁止使用内部知识库编造时间。非时间问题正常作答即可")
resp = model_with_tools.invoke([SYS_MSG, message])
print(f"resp3:{resp.content},tools:{resp.tool_calls}")

# 4、业务代码解析,本地执行工具,封装ToolMessage回传模型
my_tools = {"get_now": get_now}
if resp.tool_calls:
    for tc in resp.tool_calls:
        bingo_tool = my_tools[tc["name"]]
        tool_result = bingo_tool.invoke(tc["args"])
        # ToolMessage 的 tool_call_id 必须与上游 AIMessage.tool_calls 里的 id 一一对应
        tools_msg = ToolMessage(content=tool_result, tool_call_id=tc["id"])
        # 消息顺序必须严格:System -> Human -> AIMessage(带 tool_calls) -> ToolMessage
        resp = model_with_tools.invoke([SYS_MSG, message, resp, tools_msg])
        print(f"resp4:{resp.content},tools:{resp.tool_calls}")

程序输出结果

python 复制代码
resp1:根据系统信息,当前日期是 **2026年5月9日**。
具体时刻(时:分)建议您直接查看设备上的时钟。
resp2:,tools:[{'name': 'get_now', 'args': {}, 'id': 'call_00_bbxyVyybiajx2zp4V3Xy1805', 'type': 'tool_call'}]
resp3:,tools:[{'name': 'get_now', 'args': {}, 'id': 'call_00_MysgjGzALwnpzAUNYSGy5761', 'type': 'tool_call'}]
resp4:当前时间是 **2026年9月1日 09:11:08**。,tools:[]

逐段现象解析

resp1:不带工具Schema直接提问,模型输出幻觉时间

输出:2026年5月9日,来自模型训练截止的静态知识库。

大模型本身没有访问本机/服务器实时系统时间的能力,时间类查询极易产生幻觉;没有工具Schema的前提下,只能输出知识库脑补答案。

resp2:bind_tools 绑定工具,无 SystemMessage

content 为空字符串,tool_calls 数组有值

这是 OpenAI 兼容协议标准行为:

当模型选择输出工具调用指令,AIMessage 的 content 字段为空,填充 tool_calls 数组,告知业务层:应当调用哪个工具、入参、call_id。

⚠️重点:这里仅仅是模型输出的结构化指令,

get_now() 完全没有被执行;执行动作100%交给业务层Python代码

resp3:增加 SystemMessage 系统提示

SystemMessage 属于软性行为提示,给到模型作为输出参考,多数场景下会引导模型输出 tool_calls,减少编造时间。

关键:bind_tools() 只是把工具Schema塞进HTTP请求体,不会强制模型输出工具调用指令。SystemMessage 只是软引导,不是强约束;即便给了提示,部分模型仍然会绕过工具,直接输出编造答案。生产环境不能只靠提示词兜底。

resp4:业务层执行本地工具 + ToolMessage 回传,拿到最终回答

完整 Tool-Call 链路:

  1. 业务代码解析 AIMessage 的 tool_calls;
  2. 本地 Python 执行对应工具函数,拿到真实结果;
  3. 构造 ToolMessage,tool_call_id 必须和上游 AIMessage.tool_calls 里面的 id 一一对应;
  4. 组装完整消息列表:SYS_MSG, HumanMessage, AIMessage(携带 tool_calls), ToolMessage,再次调用LLM;
  5. LLM结合工具返回的原始数据,整理成自然语言输出。

⚠️协议硬性约束:

ToolMessage

(对应协议 role:tool)

必须紧跟一条携带 tool_calls 的 AIMessage

。如果消息顺序错乱、缺少前置的 AIMessage,OpenAI兼容接口直接返回400:

Messages with role 'tool' must be a response to a preceding message with 'tool_calls'

新手高频踩坑清单(LangChain1.3.18)

坑1:invoke 参数类型混淆

❌错误:直接传入单个消息对象

python 复制代码
resp = model.invoke(HumanMessage(content="查询当前时间"))
# ValueError: Invalid input type

✅正确:消息对象必须放到列表;字符串可以裸传

python 复制代码
resp = model.invoke([HumanMessage(content="查询当前时间")])
resp = model.invoke("查询当前时间")

invoke() 入参两种合法形式:原始字符串;listBaseMessage。SystemMessage/HumanMessage/AIMessage/ToolMessage 都属于BaseMessage子类,必须包装进列表。

坑2:SystemMessage 写在模型初始化参数

❌错误

python 复制代码
model = init_chat_model(
    ...
    system="xxx" # init_chat_model 没有system初始化参数!
)

✅正确:SystemMessage 属于会话消息,每次 invoke 随消息列表传入;模型实例是无状态,不会保存system提示。

坑3:篡改 HumanMessage.content 存放工具返回结果

不要图省事,把工具返回字符串追加到用户 HumanMessage 的 content。

❌错误:message.content += tool_result

OpenAI兼容接口识别不到这是工具执行结果,只会当成普通用户文本,工具会话协议完全失效。工具输出必须使用独立 ToolMessage 对象承载。

坑4:消息列表顺序错乱,触发400报错

报错:Messages with role 'tool' must be a response to a preceding message with 'tool_calls'

根因:消息数组缺失上一轮携带 tool_calls 的 AIMessage 对象。

✅消息固定顺序:

SystemMessage, HumanMessage, AIMessage(带 tool_calls), ToolMessage

写 LangGraph 的时候修改 state"messages",同样要严格遵守该顺序,否则下游OpenAI兼容接口直接400。

坑5:无脑全量走 Tool-Call 二次LLM请求

例如获取时间这类场景:工具返回本身就是可读最终答案,不需要再丢给大模型复述一遍。

工程上可以做分支判断,减少一次不必要LLM请求:

python 复制代码
if resp.tool_calls:
    tc = resp.tool_calls[0]
    if tc["name"] == "get_now":
        # 工具返回即可直接对外输出,跳过第二轮LLM请求
        res = my_tools[tc["name"]].invoke(tc["args"])
        print(res)
    else:
        # 查询数据库、查股价,原始数据需要模型做摘要改写,才走ToolMessage回传完整链路
        ...

坑6:对 tool_choice="required" 的错误理解

❌错误示范:在if-else内反复调用 bind_tools 传入 tool_choice="required"。

bind_tools

返回全新模型实例,每次bind都会把全部tools schema重新打包进请求;

tool_choice="required"

只强制输出任意一个工具调用,不能指定具体某一个工具

三个tool_choice枚举语义:

  • auto:模型自主选择输出工具调用或者直接回答(默认)
  • required:必须输出至少一个工具调用指令,工具任意选,不能直接输出文本回答
  • tool_choice="get_now":直接传工具名字符串,强制指定调用某一个确切工具(LangChain 底层转成 OpenAI 协议的 {"type":"function","function":{"name":"get_now"}}),想要强制调用get_now应当使用该形式

⚠️不要全局无条件开启 tool_choice="required",闲聊场景会被强制输出工具调用,业务异常。

工程正确方案:

  1. SystemMessage 做软性提示引导;
  2. 启动时预创建两个绑定实例:普通对话用默认 auto;意图识别命中时间类查询时,切换到 bind_tools(tool_list, tool_choice="get_now") 的强制实例,精确锁定该工具。请求路径上不做任何 bind。

坑7:不要幻想 SystemPrompt 100%触发工具

即便下发完整Schema + System提示,部分场景模型依然会绕过工具直接幻觉输出。不能把业务可靠性完全寄托提示词,上层意图识别 + 精确指定tool_choice作为兜底。

Tool-Call 标准完整流转图

补充:AgentExecutor、LangGraph 本质就是封装这套消息循环,底层消息协议完全不变。

用 create_agent 直接交互:框架自动跑消息循环

前面手动解析 tool_calls、本地执行、封装 ToolMessage 回传的全套循环,LangChain 1.x 已经封装成预构建 agent:create_agent(基于 LangGraph 图实现,从 langchain.agents 导入)。直接把模型实例和工具列表交给它:

python 复制代码
from langchain.agents import create_agent

my_agent = create_agent(model=model, tools=[get_now])
resp = my_agent.invoke({"messages":[message]})

print("===== 消息流转拆解 =====")
for idx, msg in enumerate(resp["messages"]):
    t = type(msg).__name__
    print(f"\n[{idx}] {t}")
    if hasattr(msg, "tool_calls") and msg.tool_calls:
        print(f"👉触发工具调用: {msg.tool_calls[0]['name']}")
    if t == "ToolMessage":
        print(f"🔧工具返回: {msg.content}")
    if hasattr(msg, "content") and msg.content:
        print(f"📝内容: {msg.content}")

# 拿最终输出
ans = resp["messages"][-1].content
print(f"\n🎯最终回答:{ans}")

agent 内部执行的正是前面流转图的六步,返回的 resp"messages" 就是完整消息链路,与手写循环产出的消息序列完全一致:

  1. HumanMessage:用户提问「查询当前时间」;
  2. AIMessage:content 为空字符串,tool_calls 数组携带 get_now 调用指令,对应前面 resp2、resp3 的输出;
  3. ToolMessage:框架自动执行 get_now(),content 是真实系统时间,tool_call_id 与上游 AIMessage 一一对应;
  4. AIMessage:结合工具结果生成最终自然语言回答,直接取 resp"messages"-1.content。

有了 agent,业务代码不再手写消息循环;但理解底层协议依然必要:一旦涉及自定义节点编排、工具执行报错处理,绕不开 state"messages" 里的消息对象操作。

总结

  1. LangChain 只负责封装工具Schema、消息对象;工具执行全部由业务层接管,大模型永远不会运行本地Python函数,只会输出工具调用结构化指令;
  2. invoke() 入参区分字符串 / listBaseMessage,所有消息实例必须放到列表传入;
  3. ToolMessage 必须携带合法 tool_call_id,消息顺序严格遵守协议,否则OpenAI兼容接口返回400;
  4. bind_tools 只是下发工具Schema,不等于强制调用工具;

tool_choice="required":强制输出任意一个工具调用,不能指定具体工具;

tool_choice="get_now":精确强制调用指定工具;

SystemMessage属于软提示,存在概率失效,生产要结合上层意图识别做兜底;

  1. 如果工具返回原始结果已经可以直接对外输出,不需要无脑回传给LLM复述,减少不必要API开销;

  2. 后续迁移 LangGraph 智能体开发,graph节点流转本质就是这套消息对象循环处理,协议约束完全一致。

相关推荐
吴声子夜歌1 小时前
Guava——事件总线
java·网络·guava
FfHUCisI1 小时前
Golang 切片扩容策略
开发语言·数据库·golang
en.en..1 小时前
Ubuntu嵌入式开发 export环境变量
java·开发语言·数据库
攻城有术1 小时前
专项攻克——重写 Redis 依赖包方法的 6 种实现方式
java·数据库·redis·bootstrap
IvorySQL1 小时前
基于 PostgreSQL 的非线性回归原理与 AI 数据库融合实践
数据库·人工智能·postgresql
比兔代理1 小时前
住宅代理IP的出口节点调度:ASN、BGP 与 IP 段信誉机制解析
linux·服务器·网络
AAA代码批发商1 小时前
DAYS 38 TCP并发服务器模型详解
linux·网络·笔记·学习
Quanqiucard1 小时前
监控球机联网总掉线?问题可能出在物联网卡上!
网络·物联网
赵渝强老师2 小时前
【赵渝强老师】高斯数据库(openGauss)的数据库对象
数据库·postgresql·opengauss·国产数据库·高斯数据库