两种 LLM 调用写法「无上下文 VS 带上下文」深度对比(完整解析)

68 阅读9分钟

结合你提供的两段 LangChain + DeepSeek 代码,我会从核心差异、执行原理、代码逐行拆解、优缺点、底层本质四个维度,把两种写法讲透,同时解答「LLM 无状态」「记忆实现」的核心问题。

一、先明确核心前提(理解两种写法的根基)

✅ LLM 接口的本质:天生无状态所有大模型的 HTTP API 调用(包括 DeepSeek/OpenAI/ 文心一言)都是「无状态请求」—— 模型对每一次请求的处理都是独立的、孤立的,不会主动保存上一次对话的任何信息。

  • 你传什么内容,模型就基于这个内容生成结果;
  • 不传历史对话,模型就「失忆」,完全不知道之前聊过什么。

✅ 两种写法的核心区别

✦ 写法 1(无上下文):直接调用模型,每次请求只传「当前问题」,模型无记忆;✦ 写法 2(带上下文):基于 LangChain 的对话记忆组件封装,自动维护历史消息,每次请求「拼接历史 + 当前问题」传给模型,实现多轮上下文关联。


二、写法 1:「原生直调」无上下文版(无记忆,最简调用)

✅ 完整代码回顾

import { ChatDeepSeek } from '@langchain/deepseek';
import 'dotenv/config';

// 1. 初始化大模型实例
const model = new ChatDeepSeek({
    model: "deepseek-chat",
    temperature: 0,       
});

// 2. 两次独立调用
const res = await model.invoke('我叫陈昊,喜欢喝白兰地');
console.log(res.content);

const res2 = await model.invoke('我叫什么名字');
console.log(res2.content);

✅ 执行原理 & 运行结果

1. 核心逻辑

这是 LLM 最基础的调用方式:直接用 model.invoke(单条提问) 发起请求,两次调用之间没有任何关联

  • 第一次调用:仅向模型传入 我叫陈昊,喜欢喝白兰地,模型正常回复;
  • 第二次调用:仅向模型传入 我叫什么名字,模型完全不知道上一轮的信息。

2. 必然的运行结果

// res.content 输出(正常回应)
你好呀陈昊,很高兴认识你~白兰地口感醇厚,确实是很经典的烈酒呢!

// res2.content 输出(核心:完全失忆)
你还没有告诉我你的名字哦,可以和我说一下呀 😊

3. 代码核心特点(逐行拆解)

  • 极简结构:只有「模型初始化 + 直接调用」两步,无额外封装;
  • 无中间层:model.invoke() 直接发起 HTTP API 请求,参数就是「单条字符串」;
  • 完全独立:两次 invoke 是两个毫无关系的 HTTP 请求,模型不会共享任何数据。

✅ 写法 1 优缺点总结

✅ 优点

  1. 代码极简,上手快,适合「单轮问答」场景(比如查知识点、单次翻译、单次生成);
  2. Token 开销最小:每次只传当前问题,不会产生额外的历史消息 Token 消耗;
  3. 无额外依赖:仅需初始化模型,不依赖 LangChain 的记忆、Prompt 等组件。

❌ 缺点

  1. 无上下文记忆:致命问题,无法实现多轮对话,模型答不上「上文相关」的问题;
  2. 扩展性差:无法自定义对话格式、无法统一系统指令、无法复用对话规则。

三、写法 2:「LangChain 封装」带上下文版(有记忆,多轮对话)

✅ 完整代码回顾

import { ChatDeepSeek } from '@langchain/deepseek';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { RunnableWithMessageHistory } from '@langchain/core/runnables';
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import 'dotenv/config';

// 1. 初始化大模型实例
const model = new ChatDeepSeek({ model: 'deepseek-chat', temperature: 0 });

// 2. 定义 Prompt 模板(核心:预留历史消息占位符)
const prompt = ChatPromptTemplate.fromMessages([
    ['system', "你是一个有记忆的助手"], // 系统指令,固定角色
    ['placeholder', "{history}"],        // 历史消息占位符(核心!)
    ['human', "{input}"]                 // 当前用户输入占位符
])

// 3. 组装执行链路:Prompt模板 → 模型
const runnable = prompt
    .pipe((input) => { // 调试节点:打印最终传给模型的完整内容
        console.log(">>> 最终传给模型的信息(Prompt 内存)");
        console.log(input)
        return input;
    })
    .pipe(model);

// 4. 初始化「内存级对话历史存储器」
const messageHistory = new InMemoryChatMessageHistory();

// 5. 封装「带记忆的可运行链路」(核心组件)
const chain = new RunnableWithMessageHistory({
    runnable,          // 基础执行链路(Prompt+模型)
    getMessageHistory: async () => messageHistory, // 指定历史存储器
    inputMessagesKey: 'input', // 绑定「当前输入」的键名
    historyMessagesKey: 'history', // 绑定「历史消息」的键名
});

// 6. 带会话ID的多轮调用
const res1 = await chain.invoke({ input: '我叫陈昊,喜欢喝白兰地' }, {
    configurable: { sessionId: 'makefriend' }
})
console.log(res1.content);

const res2 = await chain.invoke({ input: '我叫什么名字' }, {
    configurable: { sessionId: 'makefriend' }
})
console.log(res2.content);

✅ 执行原理 & 运行结果

1. 核心逻辑(最关键)

这种写法的本质是:借助 LangChain 组件,自动帮你「维护对话历史 + 拼接历史消息」,解决 LLM 「无状态」的痛点,流程如下:✅ 第 1 步:第一次调用 chain.invoke

  • 系统将 input: 我叫陈昊,喜欢喝白兰地 传入 Prompt 模板;
  • 此时无历史消息,模板最终拼接为:[{role:system, content:你是一个有记忆的助手}, {role:human, content:我叫陈昊,喜欢喝白兰地}]
  • 传给模型生成回复后,自动把「用户提问 + 模型回复」存入 messageHistory 存储器

✅ 第 2 步:第二次调用 chain.invoke

  • 系统读取 messageHistory 中的历史消息;

  • 自动拼接「系统指令 + 历史消息 + 当前提问」,最终传给模型的完整内容为:

    [    {role: "system", content: "你是一个有记忆的助手"},    {role: "human", content: "我叫陈昊,喜欢喝白兰地"},    {role: "assistant", content: "上一轮模型的回复内容"},    {role: "human", content: "我叫什么名字"}]
    
  • 模型基于「完整的上下文」生成回复,自然就能答出你的名字。

2. 必然的运行结果

plaintext

// res1.content 输出
你好陈昊!白兰地果香浓郁、口感顺滑,确实是一款很有格调的饮品呢~

// res2.content 输出(核心:精准记忆)
你叫陈昊呀 😊

3. 核心组件拆解(逐模块讲透,看懂就会用)

这是 LangChain 实现「记忆」的核心,也是两种写法的最大差异,每个组件各司其职,缺一不可

✦ 组件 1:ChatPromptTemplate 对话模板(格式约定)

typescript

运行

ChatPromptTemplate.fromMessages([    ['system', "你是一个有记忆的助手"],
    ['placeholder', "{history}"],
    ['human', "{input}"]
])
  • 作用:统一对话的格式规范,定义「系统指令、历史消息、当前提问」的固定顺序;
  • 关键:{history} 是历史消息占位符,LangChain 会自动把存储的历史对话填充到这里;
  • 优势:后续修改角色、补充规则,只需改模板,无需改调用逻辑。
✦ 组件 2:InMemoryChatMessageHistory 内存存储器(存历史)
  • 作用:临时保存对话历史(用户提问 + 模型回复),数据存在「内存」中,程序重启后丢失;
  • 替代方案:生产环境可替换为「Redis / 数据库」存储器,实现历史持久化。
✦ 组件 3:RunnableWithMessageHistory 记忆链路封装(核心)

这是实现「上下文关联」的核心封装类,核心能力:

  1. 自动读取「存储器」中的历史消息;
  2. 自动将「历史消息 + 当前输入」填充到 Prompt 模板;
  3. 自动将本次的「用户输入 + 模型回复」追加到存储器;
  4. 通过 sessionId 实现多会话隔离(比如同时开 2 个对话窗口,历史互不干扰)。

✅ 写法 2 优缺点总结

✅ 优点

  1. 支持多轮上下文记忆:完美解决 LLM 无状态痛点,实现连贯对话;
  2. 标准化、可扩展:Prompt 模板统一管理格式,可灵活添加系统指令、角色设定;
  3. 会话隔离:通过 sessionId 区分不同用户 / 不同对话,历史互不污染;
  4. 可调试:支持中间节点(如代码中的 pipe(debug)),查看传给模型的完整内容。

❌ 缺点

  1. 代码复杂度更高,需要理解 LangChain 核心组件;
  2. Token 开销随对话递增:历史消息会「滚雪球」式拼接,对话越长,单次请求的 Token 越多,成本越高;
  3. 内存存储器「非持久化」:程序重启后历史丢失,生产需额外适配持久化方案。

三、两种写法「核心维度」全方位对比表

对比维度写法 1(无上下文・原生直调)写法 2(带上下文・LangChain 封装)
核心能力仅支持「单轮问答」,无记忆支持「多轮对话」,有上下文记忆
LLM 调用方式直接调用 model.invoke(单条内容)封装后调用 chain.invoke({input})
历史消息处理完全不处理,无历史拼接自动维护、自动拼接历史消息
Token 开销极小(仅传当前问题)递增(历史 + 当前,滚雪球)
代码复杂度极简(2 步:初始化 + 调用)稍复杂(需封装模板 / 存储器 / 链路)
扩展性差(无法统一格式、无角色设定)强(模板可改、存储器可替换、支持多会话)
适用场景单轮独立问答(查资料、单次生成)多轮连贯对话(智能客服、聊天机器人)
本质区别单次无状态 HTTP 请求「存历史 + 拼历史 + 发请求」的自动化流程

四、关键补充:解答你文中提到的「核心疑问」

✅ 疑问 1:为什么 LLM API 调用是无状态的?

  • 从服务端角度:大模型的推理计算成本极高,服务端不会为每个用户「长期保存会话」(会耗尽服务器资源);
  • 从接口设计角度:HTTP 协议本身就是「无状态」的,一次请求对应一次响应,服务端不保留请求上下文。

✅ 疑问 2:让 LLM 有记忆的「底层本质」是什么?

无论用不用 LangChain,实现记忆的核心逻辑完全一致

✅ 手动 / 自动 维护一个 messages 数组 → 每次调用 LLM 时,把 messages 完整传给模型 → 模型基于数组内的所有内容生成回复 → 把本次「用户输入 + 模型回复」追加到 messages

你的文中代码也印证了这一点:

messages = [
    {role: 'user', content: '我叫陈昊,喜欢喝白兰地'},
    {role: 'assistant', content: '----------'},
    {role: 'user', content: '你知道我是谁吗?'}
]

👉 LangChain 的作用只是帮你自动化完成上述流程,不用自己手动拼接数组、手动追加历史,简化开发。

✅ 疑问 3:「滚雪球」Token 开销大,怎么解决?

这是多轮对话的通病,有 3 种主流解决方案:

  1. 历史消息截断:只保留最近 N 轮对话,超出部分丢弃(LangChain 内置该能力);
  2. 历史消息总结:对过长的历史做「摘要压缩」,用一句话概括之前的对话,减少 Token;
  3. 使用支持「向量记忆」的组件:将历史对话存入向量库,只把「和当前问题相关」的历史拼接,而非全部。

五、最终总结(快速选型指南)

✅ 选写法 1(原生直调),如果你的需求是:

单轮问答、无上下文要求、追求极简代码、控制 Token 成本(比如:翻译一句话、查一个知识点、生成一段文案)。

✅ 选写法 2(LangChain 封装),如果你的需求是:

多轮连贯对话、需要上下文记忆、需要统一角色设定、需要多会话隔离(比如:智能客服机器人、聊天助手、AI 陪伴)。

✅ 核心一句话

两种写法的本质差异,是是否为 LLM 请求「拼接历史消息」;LangChain 没有创造新原理,只是把「维护历史、拼接消息」的繁琐工作做了标准化封装,让开发者更高效地实现多轮对话。