消息(Message)是 LangChain 构建大模型交互、对话管理、工具调用与多模态能力的最小数据单元。BaseMessage 作为所有消息类型的抽象基类,定义了标准化结构、序列化规范与扩展机制,直接决定对话流程的稳定性、可观测性与跨模型兼容性。本文从底层设计、属性体系、消息家族、序列化与反序列化、工具调用场景、工程最佳实践六个维度,对 LangChain Message 机制进行系统性解析,为复杂 LLM 应用开发提供理论与实践支撑。
1 Message 在 LangChain 架构中的核心地位
LangChain 的核心能力围绕大模型交互展开:
- 对话历史管理
- 系统提示与用户指令分离
- 多轮工具调用(函数调用)
- 多模态输入输出
- 日志、追踪、重试与异常处理
这些能力均依赖统一、可扩展的消息结构。BaseMessage 承担三个关键使命:
脱离 Message 体系,LangChain 的 Chain、Agent、Memory、Callback 均无法稳定工作。
2 BaseMessage 底层设计与核心契约
BaseMessage 位于 langchain_core.messages,是抽象基类(ABC),基于 Pydantic 实现强类型校验。
2.1 设计原则
2.2 核心抽象接口
所有子类必须实现:
- content:消息主体
- type:唯一类型标识(用于反序列化)
- additional_kwargs:扩展载荷
提供通用能力:
- to_dict() / from_dict():序列化
- pretty_print():格式化输出
- copy():浅拷贝
- 支持 text、content_blocks 等派生属性
3 Message 完整属性体系(官方标准定义)
LangChain Core 最新版定义的标准属性如下:
| content | str | list[str | dict] | 消息内容:文本/多模态/结构化块 |
| type | str | 唯一消息类型标识:human/ai/system/tool/function |
| additional_kwargs | dict | 模型原生扩展字段:tool_calls、function_call 等 |
| response_metadata | dict | 响应元数据:token、模型名、请求ID、日志概率 |
| name | str | None | 角色名/函数名/发送者标识 |
| id | str | None | 消息唯一ID(通常由模型返回) |
| model_config | ConfigDict | Pydantic 配置(内部使用) |
| content_blocks | list[ContentBlock] | 标准化内容块(自动解析) |
| text | TextAccessor | 纯文本提取器,兼容 str/list 格式 |
3.1 关键属性边界
content ≠ text
- content 可包含图片、文件、工具结构、富文本。
- text 只提取纯文本,用于日志、检索、摘要。
additional_kwargs vs response_metadata
- additional_kwargs:输入侧扩展,发给模型。
- response_metadata:输出侧元数据,来自模型返回。
type 是反序列化的唯一依据
从字典恢复消息对象时,只依赖 type,不依赖类名或路径。
4 LangChain 标准消息家族与适用场景
| HumanMessage | human | 用户输入/提问 |
| AIMessage | ai | 模型回复/工具调用决策 |
| SystemMessage | system | 系统提示、角色定义 |
| ToolMessage | tool | 工具执行结果(官方推荐) |
| FunctionMessage | function | 兼容旧版函数调用 |
| ChatMessage | chat | 自定义角色消息 |
4.1 工具调用场景的消息流(最复杂且最关键)
标准 Agent 执行流程:
这也是你实际遇到 [AI]: None 的根本原因:
工具调用模式下,AI 回复的文本内容为空,决策存在于 tool_calls 中。
5 序列化与反序列化机制(生产环境必备)
5.1 标准序列化
msg_dict = msg.to_dict()
输出结构固定包含:
- role/type
- content
- additional_kwargs
- response_metadata
- name
5.2 反序列化(关键:只认 type)
from langchain_core.messages import messages_from_dict
msgs = messages_from_dict(msg_list)
底层根据 type 自动映射到对应类,保证跨版本、跨服务兼容。
5.3 空 content 场景的序列化安全
- content=""、content=None、content=[] 均合法。
- 反序列化不会崩溃,但业务逻辑必须判断空内容。
6 工程实践:空内容消息与工具调用的正确处理
6.1 核心判断模式
# 是否为纯工具调用消息
is_tool_call = (
msg.type == "ai"
and not msg.content.strip()
and "tool_calls" in msg.additional_kwargs
)
# 是否有有效文本
has_valid_text = msg.text and msg.text.strip()
6.2 稳定打印方法(适配生产日志)
def format_msg(msg: BaseMessage) –> dict:
return {
"type": msg.type,
"text": msg.text or "",
"has_tool_calls": "tool_calls" in msg.additional_kwargs,
"tool_count": len(msg.additional_kwargs.get("tool_calls", [])),
"name": msg.name,
"token_usage": msg.response_metadata.get("token_usage", {})
}
6.3 对话历史清洗规则
7 最佳实践与避坑指南
永远不要直接判断 isinstance(msg, AIMessage)
反序列化后可能是动态类,应使用 msg.type == "ai"。
不要修改 content 与 additional_kwargs 原值
使用 model_copy、copy 生成新对象。
日志只记录 text,不记录原始 content
避免多模态结构污染日志。
工具调用优先使用 ToolMessage,不使用 FunctionMessage
FunctionMessage 已逐步被弃用。
所有对话历史必须可序列化
禁止放入无法序列化的对象(如文件句柄、数据库连接)。
8 总结
BaseMessage 是 LangChain 架构中最基础、最稳定、最通用的抽象。
- 它统一了大模型交互的数据格式;
- 支撑对话、工具、多模态、记忆、追踪全链路;
- 空 content 是工具调用的正常形态,而非异常;
- 工程化必须依赖 type、text、tool_calls 进行稳健判断。
深入理解 Message 体系,是开发高可靠、可观测、可维护 LLM 应用的前提。
import asyncio
import logging
import pprint
from app.agent import create_agent
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger(__name__)
def print_result(result):
if "messages" in result:
for msg in result["messages"]:
msg_type = msg.type
print(f"\\n[{msg_type.upper()}]:\\n{msg.pretty_print()}\\n")
else:
pprint.pprint(result)
async def main():
agent = create_agent()
logger.info("Writing memory to Redis…")
result = await agent.ainvoke(
{
"messages": [
{
"role": "user",
"content": "Write 'hello redis memory' to /memories/test.txt"
}
]
}
)
logger.info("Write file result:")
print_result(result)
logger.info("Reading memory…")
result = await agent.ainvoke(
{
"messages": [
{
"role": "user",
"content": "Read the file /memories/test.txt"
}
]
}
)
logger.info("Read file result:")
print_result(result)
if __name__ == "__main__":
asyncio.run(main())
# 2026-03-12 13:24:59,797 [INFO] Writing memory to Redis…
# 2026-03-12 13:25:03,826 [INFO] HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
# 2026-03-12 13:25:10,038 [INFO] HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
# 2026-03-12 13:25:13,003 [INFO] HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
# 2026-03-12 13:25:17,014 [INFO] HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
# 2026-03-12 13:25:20,814 [INFO] HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
# 2026-03-12 13:25:20,823 [INFO] Write file result:
# ================================ Human Message =================================
# Write 'hello redis memory' to /memories/test.txt
# [HUMAN]:
# None
# ================================== Ai Message ==================================
# Tool Calls:
# write_file (call_6ZbYGfEqkShaEQfXGokDfBpA)
# Call ID: call_6ZbYGfEqkShaEQfXGokDfBpA
# Args:
# file_path: /memories/test.txt
# content: hello redis memory
# [AI]:
# None
# ================================= Tool Message =================================
# Name: write_file
# Cannot write to /memories/test.txt because it already exists. Read and then make an edit, or write to a new path.
# [TOOL]:
# None
# ================================== Ai Message ==================================
# Tool Calls:
# edit_file (call_rwMCTlI6JkWoObRelbd5QBJG)
# Call ID: call_rwMCTlI6JkWoObRelbd5QBJG
# Args:
# file_path: /memories/test.txt
# old_string:
# new_string: hello redis memory
# [AI]:
# None
# ================================= Tool Message =================================
# Name: edit_file
# Error: String '' appears 19 times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context.
# [TOOL]:
# None
# ================================== Ai Message ==================================
# Tool Calls:
# read_file (call_aw5SJ4rzbczVOSkqoMbP3r9U)
# Call ID: call_aw5SJ4rzbczVOSkqoMbP3r9U
# Args:
# file_path: /memories/test.txt
# [AI]:
# None
# ================================= Tool Message =================================
# Name: read_file
# 1 hello redis memory
# [TOOL]:
# None
# ================================== Ai Message ==================================
# Tool Calls:
# edit_file (call_UVDaDo2e8cRdnfpflTevBL7s)
# Call ID: call_UVDaDo2e8cRdnfpflTevBL7s
# Args:
# file_path: /memories/test.txt
# old_string: hello redis memory
# new_string: hello redis memory
# replace_all: True
# [AI]:
# None
# ================================= Tool Message =================================
# Name: edit_file
# Successfully replaced 1 instance(s) of the string in '/memories/test.txt'
# [TOOL]:
# None
# ================================== Ai Message ==================================
# The content "hello redis memory" has been successfully written to `/memories/test.txt`.
# [AI]:
# None
# 2026-03-12 13:25:20,826 [INFO] Reading memory…
# 2026-03-12 13:25:25,682 [INFO] HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
# 2026-03-12 13:25:31,458 [INFO] HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
# 2026-03-12 13:25:31,466 [INFO] Read file result:
# ================================ Human Message =================================
# Read the file /memories/test.txt
# [HUMAN]:
# None
# ================================== Ai Message ==================================
# Tool Calls:
# read_file (call_eiNxY5q08sPhoLuwP3gRwf38)
# Call ID: call_eiNxY5q08sPhoLuwP3gRwf38
# Args:
# file_path: /memories/test.txt
# [AI]:
# None
# ================================= Tool Message =================================
# Name: read_file
# 1 hello redis memory
# [TOOL]:
# None
# ================================== Ai Message ==================================
# The file `/memories/test.txt` contains the text: "hello redis memory".
# [AI]:
# None






