为什么用 LangChain 而不是官方 SDK

从官方示例起步

让我们从openai的官方 SDK 开始,了解大模型官方 sdk 是如何调用的。

这是一次单轮对话的 api 调用示例:

javascript
1import OpenAI from 'openai'
2
3const client = new OpenAI({
4  apiKey: process.env.API_KEY,
5  baseURL: 'https://api.deepseek.com',
6})
7
8async function main(): Promise<void> {
9  const response = await client.chat.completions.create({
10    model: 'deepseek-chat',
11    messages: [
12      { role: 'system', content: '你是一个乐于助人的助手。' },
13      { role: 'user', content: '你好!' },
14    ],
15    stream: false,
16  })
17  console.log(response.choices[0].message.content)
18}
19
20main()

这段代码干净清晰,跑通 demo 没有问题。然而当功能需求多起来后,这套写法完全支撑不住。

弊端详解

1. 上下文手动维护

模型本身是无状态的,每次调用只知道本轮传入的 messages。想要多轮对话,就必须在应用层自己维护一个messages数组,每次请求都把完整历史塞进去。

这件事听起来很简单,我把messages数组保存下来,每次调用模型都去请求完整的messages传给大模型不就好了吗?

但这样做会遇到问题:

  • 对话轮次越多,messages 数组越长,token成本飙升,花费飙涨。
  • 超出模型上下文窗口(以 DeepSeek 为例是 64K)时,请求直接失败
  • 到底保留多少历史?超限时怎么截断?要不要做摘要压缩?这些全要你自己决定

一旦对话超过几十轮,这套手动维护的方式就会失控。截断策略、摘要压缩、滑动窗口,这些记忆管理的职责被完全推给了开发者。

2. 更换模型就要换代码

从 DeepSeek 切到 OpenAI、Claude 或智谱时,需要改 baseURL、model 名,有时还要换整个 SDK,甚至重写返回格式的解析逻辑。以不同服务商的差异汇总为例:

参数DeepSeekOpenAIClaude
baseURLhttps://api.deepseek.comhttps://api.openai.comhttps://api.anthropic.com
model 名deepseek-chatgpt-4.5claude-3-7-sonnet-latest
SDK 包名openaiopenai@anthropic-ai/sdk
返回内容字段choices[0].message.contentchoices[0].message.contentcontent[0].text

实际项目里,调用通常散落在多个文件里,换一次模型就要全局搜索替换。如果同时想支持"主力模型 + 备用模型"的切换逻辑,代码会迅速臃肿,且极易遗漏某处。

3. 提示只能硬编码

system 和 user 的内容直接写死在代码里,没有任何管理机制:

js
1import type { ChatCompletionMessageParam } from 'openai/resources/chat'
2
3const messages: ChatCompletionMessageParam[] = [
4  { role: 'system', content: '你是一个乐于助人的助手。' },
5  { role: 'user', content: '你好!' },
6]

现实中一个稍微复杂点的应用,可能需要客服、翻译、代码助手等多个角色,每套角色对应不同的系统提示。这些内容全部写死在代码里,改一处就要重新部署,历史版本也无从追溯。

更大的问题是无法做 A、B 测试。要对比两套提示的效果,要么复制一份代码手动切换,要么加一堆条件分支。上线后发现哪个版本效果更好,往往也说不清楚。

4. 无法调用外部工具

API 只接受文本、返回文本,不能查天气、不能搜数据库、不能执行计算。想做 Agent 或 RAG,全靠自己封装 HTTP 调用和解析逻辑。

典型场景:

  • 用户问"北京今天天气怎么样?",模型只能靠训练数据猜测,无法调实时天气 API
  • 用户问"我们公司上季度销售额多少?",模型看不到数据库,无法查询
  • 用户问"帮我算一下 12345 × 67890",复杂计算依赖外部工具时,光靠 prompt 引导并不可靠

要把 LLM 和外部工具打通,需要自己设计并实现完整的 Agent 流程:20260309100127

图中每一个环节都需要开发者手写:判断是否需要工具、选择哪个工具、构造调用参数、发起 HTTP 请求、解析返回结果、拼回 messages、再次调用 LLM。这恰好是 Agent 的核心逻辑,裸调 API 完全不管。

5. 错误处理、重试、超时都要自己写

网络抖动、限流、模型过载、token 超限等异常,官方示例里通常不涉及,生产环境却必须考虑。

常见问题包括:

  • 网络超时,需设置 timeout,实现有边界的重试逻辑
  • 限流(429 错误),需退避重试,通常要做指数退避
  • 模型过载(503 错误),需排队或切换备用模型
  • token 超限,需截断输入或提示用户缩短问题
  • API Key 失效,需友好提示和 fallback 机制

这还只是最基础的限流重试,没有覆盖超时、备用模型、熔断等场景。完整的生产级错误处理远比这复杂,而这些全是业务逻辑之外的基础设施工作。

6. 流式输出自己处理

若设 stream: true,返回的是 SSE(Server-Sent Events)流,需自己遍历异步迭代器、拼接字符串、处理断开重连:

javascript
1import OpenAI from 'openai'
2
3const client = new OpenAI({
4  apiKey: process.env.DEEPSEEK_API_KEY,
5  baseURL: 'https://api.deepseek.com',
6})
7
8const stream = await client.chat.completions.create({
9  model: 'deepseek-chat',
10  messages: [{ role: 'user', content: '写一首诗' }],
11  stream: true,
12})
13
14for await (const chunk of stream) {
15  const content = chunk.choices[0]?.delta?.content
16  if (content) {
17    process.stdout.write(content)
18  }
19}

在 Node.js 脚本里打印还好说,一旦需要把流式结果推给前端,就涉及 WebSocket 或 SSE 通道的管理、前端的渲染更新、中途断流的重连策略,以及如何在流式输出结束后触发后续逻辑(比如保存对话记录)。这些在官方示例里通常一笔带过。

总结

把以上六个问题汇总来看,裸调 API 把所有编排职责都推给了开发者,每一条单独来看都不是无法解决的问题,但六条加在一起,加上日后维护和扩展的成本,很快就会让代码变成一个难以维护的大泥球。LangChain 把这些能力封装成可复用组件,让开发者把精力放在业务逻辑上,而不是重复造基础设施。

20260313215647

一图概括裸调 API 与 LangChain 在上下文、多模型、提示、工具、错误与流式六方面的对比,以及从大泥球到可复用组件的差异。

简单来说 LangChain 就是一套提供更丰富的扩展性,更简单的维护的 LLM 模型调用框架,它在内部帮我们做了很多自动档的事情,让我们更专注于业务。