ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

LangChain Message 体系详解:从基础到工具调用实战

LangChain Message 体系详解:从基础到工具调用实战

1. 从一个被忽视的细节说起:为什么消息对象才是 LangChain 的地基

很多人上手 LangChain 的第一反应是去折腾 Chain、Agent、RAG 这些听起来很唬人的概念,结果跑通一个 Demo 之后就卡住了——多轮对话记不住上下文、模型返回的内容格式对不上、工具调用老是报错。折腾半天回头看,问题往往不在那些高级组件上,而是最底层的Message没搞明白。

LangChain 里的 Message 不是简单的字符串包装。它是一套结构化的对话协议,把"谁在说话""说了什么""有没有附带工具调用""是不是最终答案"这些信息全部编码进对象里。你后面用到的所有能力——多轮记忆、函数调用、结构化输出、流式响应——本质上都是在操作 Message 对象。把这层吃透,很多看似玄学的问题会瞬间变得清晰。

这篇内容适合三类人:刚接触 LangChain 想搞清楚基础概念的新手、被多轮对话和工具调用坑过的中级开发者、以及想从其他框架迁移过来需要对齐心智模型的老手。我会从设计动机讲到代码实操,把 SystemMessage、HumanMessage、AIMessage 这几个核心类型掰开揉碎,再补上实际项目里踩过的坑和排查套路。看完你应该能独立判断:什么时候该用哪种 Message,消息列表该怎么拼,报错该往哪个方向查。

2. Message 体系到底解决了什么问题

2.1 从裸字符串到结构化对象的演进逻辑

早期调用大模型接口,大家都是拼一个字符串丢过去,模型返回一个字符串拿回来。简单场景没问题,但只要涉及多轮对话就立刻露馅:你得自己维护一个历史列表,手动拼接"用户说:xxx\n助手说:xxx",还要处理角色标记、截断策略、特殊字符转义。更麻烦的是,当模型需要调用工具时,返回的内容里混着自然语言和结构化参数,用字符串根本没法可靠解析。

LangChain 的解法是把每条对话抽象成一个 Message 对象,每个对象至少包含两个核心字段:content(内容)和类型标识(通过类本身区分)。这样带来几个直接好处。第一,角色信息不再靠字符串约定,而是由类型系统保证,HumanMessage就代表人类输入,AIMessage就代表模型输出,不会混淆。第二,可以挂载额外元数据,比如AIMessage能携带tool_calls字段记录工具调用请求,additional_kwargs能存原始响应里的其他信息。第三,整个消息列表可以作为一个整体传给模型,框架负责转换成各家 API 需要的格式,你写一套代码就能适配不同厂商。

这个设计思路其实和 HTTP 从纯文本协议演进到带结构化头部是一个道理。纯文本能跑,但一旦要表达复杂语义,结构化就是必然选择。

2.2 四种核心 Message 类型的分工

LangChain 里最常用的 Message 类型有四种,各自职责非常明确。

SystemMessage代表系统指令,用来设定模型的角色、行为边界、输出格式要求。它通常放在消息列表的最前面,对整个对话起约束作用。比如你希望模型扮演一个严谨的技术顾问,就把这段设定写进 SystemMessage。需要注意的是,不同模型对 SystemMessage 的重视程度不一样,有些模型会严格遵循,有些则可能忽略,这一点后面会细说。

HumanMessage代表用户输入。这里有个容易踩的坑:很多人以为 HumanMessage 只能是纯文本,其实它的content字段可以是一个列表,支持多模态输入,比如同时包含文本和图片。做图文问答的时候这个特性很关键。

AIMessage代表模型返回的内容。它比前面两个复杂,因为模型可能返回纯文本,也可能返回工具调用请求,还可能两者都有。所以 AIMessage 除了content,还有tool_calls、invalid_tool_calls等字段。理解这些字段是处理工具调用的前提。

ToolMessage代表工具执行的结果,需要和触发它的那次工具调用通过tool_call_id关联起来。这个类型在构建 Agent 时必不可少,因为模型需要知道它请求的工具到底返回了什么。

Message 类型核心字段典型用途常见误区
SystemMessagecontent设定角色与规则以为所有模型都严格遵守
HumanMessagecontent用户输入忽略多模态列表格式
AIMessagecontent, tool_calls模型输出只看 content 忽略 tool_calls
ToolMessagecontent, tool_call_id工具结果回传忘记关联 id 导致报错

2.3 消息列表的顺序与角色约束

消息列表不是随便排的,大多数模型 API 对角色顺序有隐含要求。通常的规则是:SystemMessage 放最前,然后 HumanMessage 和 AIMessage 交替出现,ToolMessage 必须紧跟在触发它的 AIMessage 之后。如果你把顺序搞乱,轻则模型理解偏差,重则 API 直接报错。

我见过一个典型错误:有人为了省事,把多条 HumanMessage 连续堆在一起,中间不插入 AIMessage。这在某些模型上能跑,但会导致模型把这几条输入当成一个整体来理解,而不是多轮对话。正确的做法是保证一问一答的交替结构,除非你确实想表达"用户连续补充了几条信息"这种语义。

3. 核心类型逐个拆解与实操要点

3.1 SystemMessage:设定边界但别指望它万能

SystemMessage 的用法看起来最简单,就是塞一段指令进去。但实际项目里,它的效果受模型能力影响很大。我做过一个对比测试,同样的 SystemMessage 要求模型"只输出 JSON 格式,不要任何解释文字",在能力较强的模型上基本能稳定遵循,但在一些轻量模型上经常夹带"好的,以下是结果:"这类前缀。

所以我的经验是:SystemMessage 用来设定大方向,比如角色定位、语气风格、总体约束,但不要把所有格式要求都押在它身上。对于严格的格式输出,更可靠的做法是用结构化输出功能,或者在解析层做容错处理。另外,SystemMessage 的内容不宜过长,太长的指令会稀释重点,模型可能抓不住关键约束。如果确实有很多规则,考虑用编号列表组织,把最重要的放前面。

还有一个细节:部分模型支持在对话中途插入 SystemMessage 来动态调整行为,但这属于进阶用法,兼容性参差不齐,生产环境慎用。

3.2 HumanMessage:多模态输入的正确姿势

HumanMessage 的基础用法就是传一个字符串,这个没什么好说的。真正值得展开的是多模态场景。当你要传图片时,content需要写成列表形式,每个元素是一个字典,标明类型和内容。文本部分用{"type": "text", "text": "..."},图片部分用{"type": "image_url", "image_url": {"url": "..."}}。

这里有个实操要点:图片 URL 可以是公网地址,也可以是 base64 编码的 data URI。用公网地址方便但有隐私和稳定性风险,用 base64 则请求体体积会变大。我一般建议在测试阶段用公网地址快速验证,生产环境根据实际情况选择。另外要注意,不是所有模型都支持多模态输入,传之前确认一下目标模型的能力,否则会收到格式错误。

还有一个容易被忽略的点:HumanMessage 的 content 如果传列表,某些工具函数可能无法直接处理,比如做消息长度统计或者文本搜索时,需要先做类型判断。写通用工具函数的时候记得处理这种情况。

3.3 AIMessage:读懂 tool_calls 才算入门

AIMessage 是四种类型里最需要花时间理解的。普通情况下它就是一个 content 字符串,但一旦涉及工具调用,结构就复杂了。当模型决定调用工具时,返回的 AIMessage 里content可能是空的,而tool_calls字段会包含一个列表,每个元素描述一次工具调用,包括工具名称、参数、以及一个唯一的 id。

这个 id 非常关键。你执行完工具后,构造 ToolMessage 时必须把tool_call_id设成对应的值,模型才能把结果和请求对应起来。我踩过的坑是:早期手动构造 ToolMessage 时忘了设 id,结果模型收到结果后完全不知道这是对哪次调用的响应,要么报错要么胡言乱语。

另外,tool_calls里的参数是模型生成的,不保证一定符合你定义的 schema。所以工具函数内部一定要做参数校验,不能假设模型给的就是对的。我一般会在工具函数入口加一层校验,参数不对就返回一个明确的错误信息给模型,让它重新尝试。

3.4 ToolMessage:结果回传的关联机制

ToolMessage 的构造相对简单,核心就是content和tool_call_id两个字段。content放工具执行的结果,可以是字符串,也可以是 JSON 序列化后的文本。tool_call_id必须和触发它的那次工具调用一致。

这里有个实用技巧:工具返回的结果如果很长,考虑做截断或摘要,因为过长的结果会占用大量上下文窗口,还可能干扰模型判断。我通常会把结果控制在合理长度内,如果确实需要返回大量数据,就在工具内部先做筛选,只返回关键信息。

还有一个细节:如果工具执行失败,不要直接抛异常中断流程,而是把错误信息作为 ToolMessage 的内容返回给模型。这样模型有机会根据错误信息调整策略,比如换个参数重试,或者向用户说明情况。这种"把错误当信息"的处理方式,是构建健壮 Agent 的关键。

4. 完整实操:从零搭一个带工具调用的对话流程

4.1 环境准备与依赖安装

先把环境搭起来。我习惯用虚拟环境隔离依赖,避免版本冲突。

python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install langchain langchain-openai

这里说明一下选型理由:langchain是核心包,提供 Message 类型定义和基础抽象;langchain-openai是模型集成包,负责把 LangChain 的 Message 转换成目标 API 需要的格式。之所以拆成两个包,是因为 LangChain 采用了模块化设计,核心逻辑和具体模型解耦,你想换其他模型只需要换集成包,业务代码基本不用动。

版本方面,LangChain 迭代很快,建议锁定一个稳定版本,不要盲目追新。我一般会在 requirements.txt 里写死版本号,避免某天自动升级后代码跑不起来。

4.2 构造消息列表并调用模型

先来一个最基础的例子,把四种 Message 里的三种用上。

from langchain_openai import ChatOpenAI from langchain_core.messages import SystemMessage, HumanMessage, AIMessage llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) messages = [ SystemMessage(content="你是一个严谨的技术顾问,回答简洁,不说废话。"), HumanMessage(content="LangChain 里的 Message 有哪几种类型?"), ] response = llm.invoke(messages) print(type(response)) # <class 'langchain_core.messages.ai.AIMessage'> print(response.content) # 模型返回的文本

这段代码有几个值得注意的地方。第一,invoke返回的直接就是 AIMessage 对象,不是字符串,你可以直接访问.content拿文本,也可以访问其他字段。第二,temperature=0是为了让输出更稳定,做技术问答时我一般都会调低这个参数。第三,消息列表的顺序是 System 在前、Human 在后,这是标准结构。

如果你想继续多轮对话,就把返回的 AIMessage 追加到列表里,再加新的 HumanMessage。

messages.append(response) messages.append(HumanMessage(content="那 AIMessage 和 HumanMessage 有什么区别?")) response2 = llm.invoke(messages) print(response2.content)

这样模型就能看到之前的对话历史,回答会更有连贯性。这就是多轮对话的本质:维护一个不断增长的消息列表。

4.3 定义工具并处理工具调用

接下来是重头戏——工具调用。先定义一个简单的工具函数。

from langchain_core.tools import tool @tool def get_weather(city: str) -> str: """查询指定城市的天气。""" # 实际项目里这里会调用真实的天气 API fake_data = {"北京": "晴,25度", "上海": "多云,28度"} return fake_data.get(city, "暂无该城市数据") tools = [get_weather] llm_with_tools = llm.bind_tools(tools)

bind_tools的作用是告诉模型有哪些工具可用,模型会在需要时生成工具调用请求。注意工具函数的 docstring 很重要,模型会根据它判断什么时候该用这个工具,所以描述要清晰准确。

现在发起一次可能触发工具调用的对话。

messages = [ SystemMessage(content="你是一个助手,需要查天气时调用工具。"), HumanMessage(content="北京今天天气怎么样?"), ] ai_msg = llm_with_tools.invoke(messages) print(ai_msg.tool_calls) # [{'name': 'get_weather', 'args': {'city': '北京'}, 'id': 'call_xxx', 'type': 'tool_call'}]

如果模型决定调用工具,tool_calls里就会有内容。接下来执行工具并把结果回传。

from langchain_core.messages import ToolMessage messages.append(ai_msg) for tool_call in ai_msg.tool_calls: # 找到对应的工具并执行 tool_result = get_weather.invoke(tool_call["args"]) messages.append(ToolMessage( content=str(tool_result), tool_call_id=tool_call["id"] )) final_response = llm_with_tools.invoke(messages) print(final_response.content)

这段代码的关键在于tool_call_id的传递。每个 ToolMessage 都必须带上对应工具调用的 id,模型才能正确关联。执行完所有工具后,再次调用模型,它就会基于工具结果生成最终回答。

4.4 参数选择与上下文窗口管理

随着对话轮次增加,消息列表会越来越长,最终可能超出模型的上下文窗口。这时候需要做截断或摘要。最简单的策略是保留 SystemMessage 和最近 N 轮对话,丢弃更早的历史。但这样会丢失早期信息,对于需要长期记忆的场景不够用。

更稳妥的做法是做对话摘要:当消息数量超过阈值时,调用模型把早期对话压缩成一段摘要,用 SystemMessage 或 HumanMessage 的形式保留,然后丢弃原始消息。这样既控制了长度,又保留了关键信息。

参数选择上,max_tokens要留足空间给输出,不要把上下文窗口占满。我一般会把历史消息控制在窗口的 60% 到 70%,剩下的留给模型生成。具体数值要根据模型能力和任务复杂度调整,没有万能公式。

5. 常见报错与排查技巧实录

5.1 工具调用相关的典型错误

工具调用是报错重灾区,我整理了几个高频问题和排查方向。

报错现象可能原因排查方向
模型不调用工具工具描述不清或 bind_tools 未生效检查 docstring 是否准确,确认绑定的是带工具的实例
tool_call_id 不匹配ToolMessage 的 id 设错或缺失打印 ai_msg.tool_calls 核对 id
参数校验失败模型生成的参数不符合 schema在工具函数内加校验,返回明确错误
无限循环调用工具结果让模型误以为需要继续调用设置最大迭代次数,检查工具返回内容

我遇到过一次诡异的问题:模型反复调用同一个工具,陷入死循环。排查后发现是工具返回的内容格式有问题,模型误以为调用失败所以一直重试。把返回内容改成明确的成功标识后就正常了。这个教训是:工具返回的信息要清晰表达"成功还是失败",不要让模型去猜。

5.2 消息格式与角色顺序问题

另一类常见错误是消息格式不对。比如把 AIMessage 的 content 设成列表但格式不对,或者消息列表里出现了不支持的角色组合。这类问题通常会有比较明确的报错信息,比如提示某个字段类型错误。

排查这类问题的通用方法是:把消息列表打印出来,逐条检查类型和字段。我习惯写一个辅助函数,把消息列表格式化成易读的形式,方便调试。

def debug_messages(messages): for i, msg in enumerate(messages): print(f"[{i}] {type(msg).__name__}: {msg.content[:50]}...") if hasattr(msg, "tool_calls") and msg.tool_calls: print(f" tool_calls: {msg.tool_calls}")

这个函数在排查多轮对话和工具调用问题时特别有用,能一眼看出消息顺序和内容是否符合预期。

5.3 模型兼容性与行为差异

不同模型对 Message 的处理方式有差异,这是很多人忽略的坑。比如有些模型对 SystemMessage 的支持较弱,你设的规则它可能不完全遵守;有些模型在工具调用的参数格式上和主流标准有出入;还有些模型对多模态输入的支持有限。

我的建议是:换模型时不要假设行为一致,一定要重新测试关键流程。特别是工具调用和结构化输出这两块,不同模型的稳定性差异很大。如果项目对稳定性要求高,优先选择经过充分验证的模型,不要为了省钱或追新而冒险。

另外,某些模型返回的 AIMessage 里additional_kwargs会包含额外信息,比如原始响应、token 用量等。这些信息在调试和成本核算时很有用,值得关注。

6. 我在实际项目里的一些体会

做了一段时间 LangChain 项目,最大的感受是:Message 这层抽象看起来简单,但它是整个框架的地基,地基不牢后面全是坑。我见过太多人跳过基础直接上 Agent,结果被工具调用和上下文管理搞得焦头烂额,回头补课才发现问题都出在最基础的消息构造上。

一个实用建议:写代码时养成打印消息列表的习惯,尤其是在调试多轮对话和工具调用时。很多时候你以为的消息结构和你实际构造出来的并不一样,打印出来一看就清楚了。另外,工具函数的 docstring 要认真写,它直接影响模型的调用决策,写得含糊模型就乱调,写得清楚模型就精准。

最后分享一个小技巧:如果你发现模型对 SystemMessage 的遵循度不高,可以尝试把关键约束同时写进 SystemMessage 和第一条 HumanMessage 里,双重强调往往能提升遵循率。这个技巧在轻量模型上效果尤其明显。至于后续扩展,可以研究一下消息的序列化和持久化,把对话历史存到数据库里,这样就能支持跨会话的长期记忆了。

返回列表