扒开 LangChain 源码:为什么 Agent 里的 Message 数组可以“混搭”传参?
如果你最近在学习使用 LangChain 构建 AI Agent,你可能会在一些开源项目或者教程中,看到类似于下面这样一段让人“强迫症发作”的代码:
# 调用 Agent,发送消息response=agent.invoke({"messages":[{"role":"system","content":"你是一个热心的AI助手。"},{"role":"user","content":"你好,我是曦煜。"},{"role":"assistant","content":"你好,曦煜,很高兴认识你。"},{"role":"user","content":"北京今天天气如何?"},SystemMessage(content="请使用工具来获取天气信息。")]})如果你有一点传统的静态强类型语言(如 Java 或 C++)基础,看到这段代码一定会感到极其违和:在一个messages数组(列表)里,前四个元素是普通的 Python 字典(Dict),而最后一个元素竟然是一个实例化的对象(SystemMessage)。
这种把字典和对象混放在同一个数组里的“大杂烩”写法,如果是普通框架早就抛出类型异常了。为什么在 LangChain 中它却能平稳运行?
今天,我们就用架构师的视角,扒开 LangChain 的底层源码,看看这个“黑魔法”究竟是怎么回事。
1. 框架的“亲儿子”:原生的 Message 对象
在 LangChain 最纯正的血统架构中,所有的聊天消息都被严格抽象为BaseMessage的子类。最常见的有三个:
SystemMessage(系统提示词,定基调)HumanMessage(人类用户的输入)AIMessage(大模型的回复)
当你传入SystemMessage(content="...")时,LangChain 底层一看:“这是标准接口的亲儿子,格式极其规范,直接放行!” 这种面向对象的写法,也是 LangChain 官方最初唯一推荐的写法。
2. 框架的“干儿子”:OpenAI 风格的字典
既然有亲儿子,为什么还要允许字典存在呢?仔细看代码里的前四行:{"role": "user", "content": "你好"}
如果你熟悉 OpenAI 的原生 API,你会立刻认出:这不就是 OpenAI 官方标准的请求格式吗!
在早期的 LangChain 版本中,如果你敢直接传这种字典,框架真的会无情报错。它会逼着你必须在文件开头from langchain_core.messages import HumanMessage, SystemMessage,然后把所有从 OpenAI 官方文档里复制来的 Prompt,一行行手动改写成 LangChain 的对象。
这种极其死板的强类型约束,很快引来了社区开发者的疯狂吐槽:“为了用你 LangChain,我迁移旧代码的成本也太高了吧,简直反人类!”
3. 架构师揭秘:底层的“自动翻译官”
为了平息众怒,同时实现对整个 OpenAI 生态的完美向下兼容,LangChain 开发团队做出了妥协。他们在底层的消息入口处(如agent.invoke的内部流转机制中),加入了一个极其强大的自动拦截与类型强转(Type Coercion)机制。
在 LangChain 源码中,通常对应着一个名为convert_to_messages的工具函数。
当agent.invoke接到你传进来的这堆“大杂烩”数组时,它并不会直接发给大模型,而是先跑一个内部的遍历清洗循环:
- 遇到字典时:它会提取字典里的
role字段。如果是"system",它就在后台默默帮你new一个SystemMessage;如果是"user",它就偷偷帮你转成HumanMessage。 - 遇到对象时:它发现本来就是
BaseMessage的子类(比如代码中的SystemMessage),无需转换,直接保留。
最终,在这个数组被真正发给底层大模型(如 GPT-4 或 Claude)的物理链路之前,它已经被 LangChain 统一“洗”成了一排整整齐齐的纯对象数组。
4. 总结与最佳实践建议
看懂了这个底层逻辑,我们就明白了:这两种写法同时存在,本质上是 LangChain 框架在**“惯着开发者”**。
- 传字典:为了让你少引包、少敲键盘,方便你直接从 OpenAI 代码库无缝迁移。
- 传对象:这是框架的标准和高阶玩法。当你需要用到多模态(传图片)或者需要携带复杂的 Tool Calls(工具调用记录)时,简单的字典根本塞不下那么复杂的属性,此时必须使用原生的
HumanMessage等对象。
防坑建议:
虽然框架底层有着强大的包容心,能够完美兼容这种“混搭”写法,但在我们自己的正规企业级项目里,强烈建议保持代码风格的统一。
要么全用字典,要么全用 Message 对象。像示例中这种混着写的代码,属于典型的“坏味道(Bad Smell)”,它不仅会让 IDE 的类型推导失效,更容易让接手代码的同事感到困惑。优雅的代码,永远是从统一规范开始的!