LangChain 第一阶段学完后,我对 AI Agent 开发的 6 个核心判断

0 阅读13分钟

学完 LangChain 第一阶段之后,如果你脑子里留下的只是 ChatModelPromptTemplateOutputParsertoolmemoryRAGLCEL 这些 API 名字,其实还不算真正学会。

因为工程里最难的,从来不是“记住有哪些类”,而是搞清楚:

  • 这些组件分别解决什么问题
  • 它们在整条 AI 应用链路里处于什么位置
  • 为什么有些能力必须要上,有些能力却不该过早引入
  • 一个能跑通的 demo,怎么变成一个能维护的 Agent 系统

如果把这一阶段的内容压缩成一句话,我的判断是:

AI Agent 开发,本质上不是“调用一次模型”,而是在管理一条可控的数据流:怎么喂给模型、怎么约束模型、怎么让模型调用外部能力、怎么保留上下文、怎么接入知识、怎么把这一切编排成稳定流程。

LangChain 的价值,也正在这里。

它不是单纯的“大模型 SDK 集合”,更像是一层 AI 应用运行时:上层面向业务,下层适配模型、工具、向量库和执行链路。

这篇文章不打算按 API 清单来回顾,而是从工程视角给出我对 LangChain 第一阶段的 6 个核心判断。你只要把这 6 件事想明白,后面继续学 LangGraph、LangSmith、MCP、RAG 优化,都会更顺。


判断一:LangChain 首先解决的,不是“更方便调模型”,而是“避免业务代码和模型供应商耦死”

很多人一开始会觉得,直接调 OpenAI、Claude、Gemini 的官方 SDK 不就行了,为什么还要加一层 LangChain?

如果你只是做一次性的 demo,这个问题成立。
但只要进入真实项目,它很快就不成立。

原因很简单,不同大模型提供商虽然都叫“聊天模型”,但接口细节并不统一。

比如系统提示词的位置就不一样:

  • OpenAI 风格:放在 messages
  • Anthropic 风格:有单独的 system 字段
  • Gemini 风格:又是 system_instruction

这还只是最表面的差异。
更实际的差异还包括:

  • 流式返回格式不同
  • 工具调用字段不同
  • 多模态输入组织方式不同
  • 结构化输出支持能力不同
  • 各家模型的特有参数不同

如果业务代码直接绑定某一家接口,后果通常有两个:

  1. 切模型会越来越痛
  2. 功能一旦做深,代码里到处都是供应商分支逻辑

LangChain 在这里做的第一件事,就是把不同模型适配成统一的 BaseChatModel 抽象。

也就是说,你真正依赖的不是某一家模型的原生请求格式,而是 LangChain 统一后的调用语义。

这件事的工程价值非常大。

LangChain 统一抽象不同 ChatModel

一个最朴素的例子

import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

const result = await model.invoke("解释一下什么是 RAG");
console.log(result.content);

这段代码本身很普通,但它的意义不在“成功调到了一个模型”,而在于你的业务侧只面向统一的 ChatModel 调用方式。

这里最容易误解的一点

很多国产模型兼容 OpenAI 协议,所以用 ChatOpenAI 也能调,比如 Qwen、DeepSeek 的某些服务都支持这种方式。

但这不代表“有兼容协议就不需要专用适配类”。

原因是:

  • 兼容 OpenAI 协议,解决的是“能不能调”
  • 专用 ChatModel,解决的是“能不能把这家模型的能力用全”

如果你只是跑基础文本对话,用兼容接口没问题。
如果你需要充分利用某个模型的专有特性,比如更细粒度的工具调用、多模态、特殊采样参数,那专用适配类通常更稳。

所以我的建议很明确:

  • 快速验证阶段:优先统一走 LangChain ChatModel 抽象
  • 真正选型落地阶段:能用专用 ChatModel 就尽量用专用适配

这不是“写法偏好”,而是为了给模型切换和能力扩展留下余地。


判断二:Prompt 工程的本质不是写文案,而是管理模型上下文

很多团队一开始做 AI 功能,最先做的是拼字符串:

const prompt = `
你是企业知识库助手。

用户问题:
${question}

检索资料:
${docs}

历史对话:
${history}
`;

这段代码能跑,但只适合 demo。

因为真实项目里,Prompt 不是一段静态文本,而是一层持续演化的上下文组织系统。它至少会同时承载这些信息:

  • 角色设定
  • 业务规则
  • 当前任务
  • 用户输入
  • 历史对话
  • 检索到的上下文
  • Few-shot 示例
  • 输出格式约束

如果这些东西全都混在一个大字符串里,早晚会出事:

  • 角色和规则改动时不敢动
  • 历史消息和检索片段互相污染
  • token 被谁吃掉了根本看不出来
  • 一个 Prompt 改好了,另一个场景还在复制旧版本

这也是为什么我更推荐把 Prompt 看成“上下文组装层”,而不是“提示词文本”。

ChatPromptTemplate 解决的核心问题

如果你用的是聊天模型,默认就应该优先考虑 ChatPromptTemplate

PromptTemplate 与输入输出控制在链路中的位置

import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";

const qaPrompt = ChatPromptTemplate.fromMessages([
  [
    "system",
    `你是企业内部知识库助手。
1. 优先依据资料回答
2. 资料不足时明确说明不知道
3. 不要编造流程细节`,
  ],
  new MessagesPlaceholder("history"),
  [
    "human",
    `问题:{question}

资料:
{context}`,
  ],
]);

这段代码的价值不是“写法更优雅”,而是上下文来源被明确分层了:

  • system 放角色和高优先级规则
  • history 放会话记忆
  • human 放当前问题和动态上下文

这让 Prompt 从“大字符串”变成了“可维护结构”。

什么时候要用 PipelinePromptTemplate

一旦 Prompt 开始模块化,比如你要把下面这些块拆开复用:

  • 企业角色设定
  • 部门级规则
  • 输出格式要求
  • 任务描述

这时 PipelinePromptTemplate 就有价值了。

它不是为了让模型“更聪明”,而是为了让 Prompt 资产可以像代码一样复用。

PipelinePromptTemplate 组合多个 Prompt 模块

Few-shot 什么时候该上,什么时候别乱上

Few-shot 很常见,但也最容易被滥用。

适合 Few-shot 的场景:

  • 输出格式有微妙约束
  • 需要模型学习风格或边界
  • 同一类任务存在稳定范式

不适合滥加 Few-shot 的场景:

  • 检索上下文已经很长
  • 示例和当前任务相似度不高
  • 你只是想“多塞点东西试试”

因为 Few-shot 的本质是用上下文换模型行为一致性。
它不是免费的,代价就是上下文窗口和推理成本。

所以我的判断是:

Prompt 工程里真正重要的,不是句子写得多华丽,而是把哪些信息该进 Prompt、以什么结构进 Prompt、优先级如何排序,这些事情管清楚。


判断三:输出控制不只是“解析 JSON”,而是在给模型返回结果建立契约

AI 应用一旦进入业务系统,模型输出就不能一直停留在“自然语言挺像那么回事”的阶段。

因为你最终很可能要把结果继续交给:

  • 前端表单
  • 数据库存储
  • 工作流系统
  • 下游工具调用
  • 审批或风控规则

这时“能看懂”不够,必须“结构稳定”。

LangChain 第一阶段里,输出控制这块非常关键。
我认为最值得建立的认知是:

结构化输出解决的,不是解析方便,而是把模型输出变成系统契约。

结构化输出通常依赖 tool call 或 JSON schema

首选方案通常不是手写 JSON prompt,而是 withStructuredOutput

如果模型支持 tool calling 或 JSON schema,优先用模型原生能力约束输出。

import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

const ticketExtractor = model.withStructuredOutput(
  z.object({
    title: z.string().describe("工单标题"),
    category: z.enum(["bug", "feature", "question"]).describe("工单类型"),
    priority: z.enum(["low", "medium", "high"]).describe("优先级"),
  })
);

const result = await ticketExtractor.invoke(
  "用户反馈订单页面在手机端无法滚动,影响下单,优先处理。"
);

这样做的默认好处是:

  • 结构更稳定
  • 少依赖 Prompt 约束
  • 对接业务系统时更可靠

那 OutputParser 还有没有价值

有,而且在两个场景下很重要:

1. 模型不支持原生结构化输出

这时你就要退回到“Prompt 里写格式要求 + Parser 解析”的方案。

2. 你需要流式消费结构化片段

这在 Agent 场景里尤其常见。
比如流式工具调用时,参数往往会被拆成很多碎片返回,自己手动拼 JSON 很麻烦,这时像 JsonOutputToolsParser 这样的工具就很有价值。

几类 Parser 的职责差异,最好从“适用场景”来理解

  • StringOutputParser:你只要最终文本
  • StructuredOutputParser:你要一个稳定 JSON 结构,但模型不一定原生支持
  • XMLOutputParser:输出必须是 XML 之类的指定格式
  • JsonOutputToolsParser:重点不是一般文本,而是 tool call 的结构化片段,尤其适合流式

一个常见误区

很多人分不清“结构化输出”和“工具调用”的边界。

这两个虽然都可能产生结构化 JSON,但不是一回事:

  • 结构化输出:目标是“稳定返回数据”
  • 工具调用:目标是“让模型发起一个动作”

如果你的需求只是“把用户输入抽成对象”,优先用结构化输出。
如果你的需求是“让模型决定要不要触发外部能力”,那才是 tool call。

这是一个很典型的选型边界。


判断四:Tool 和 MCP 的本质,不是“给模型加插件”,而是把模型从纯生成升级成可执行系统

很多人学到 tool call 时会非常兴奋,因为它看起来像是大模型“终于可以干活了”。

这个感觉没错,但如果要说得更准确一点,我会这样定义:

Tool 机制的本质,是把模型从“文本生成器”升级成“动作决策器”。

模型不再只是返回一段答案,而是可以输出一个结构化动作意图:

  • 调哪个工具
  • 传什么参数
  • 工具结果返回后再怎么继续推理

这就是 Agent 的起点。

最小可用的 Tool 调用链路

import { z } from "zod";
import { tool } from "@langchain/core/tools";

const searchPolicy = tool(
  async ({ keyword }) => {
    return `检索结果:关于「${keyword}」的制度文档共 3 篇。`;
  },
  {
    name: "search_policy",
    description: "检索企业制度文档",
    schema: z.object({
      keyword: z.string().describe("检索关键词"),
    }),
  }
);

const modelWithTools = model.bindTools([searchPolicy]);

这时模型就具备了调用 search_policy 的能力。

但真正重要的不是 bindTools 这一步,而是后面的执行闭环:

  1. 模型返回 tool_calls
  2. 系统执行工具
  3. 把结果封装成 ToolMessage
  4. 再喂回模型继续推理
  5. 直到模型不再发起新调用

也就是说,tool call 不是“一次函数调用”,而是一个闭环过程。

tool call 的核心是“模型决策 -> 工具执行 -> 结果回写 -> 再次推理”

为什么 Tool 描述和参数 schema 这么重要

因为模型并不真正理解你的函数实现,它只看两样东西:

  • 工具描述
  • 参数定义

所以很多 tool 调不准,不是模型太笨,而是工具契约写得不清楚。

一个很实用的经验是:

  • name 要明确,不要太抽象
  • description 要写清楚何时该用、不该用
  • schema 字段描述要站在模型视角写,而不是站在开发者视角写

MCP 为什么重要

自己定义 tool 解决的是“我手里已有的本地能力”。
MCP 解决的是“别人已经实现好的能力,我能不能标准化接进来”。

这件事对 Agent 生态特别关键。

因为现实里很多高价值能力并不是你自己写的,而是已经由:

  • 地图服务
  • 浏览器控制
  • 文件系统
  • 数据平台
  • IDE / 编辑器

这些外部系统提供出来。

MCP 相当于给“外部可调用能力”建立了一套统一接入方式。
在 LangChain 里,@langchain/mcp-adapters 则把这层能力转成了可以被模型绑定和调用的工具集合。

Tool 和 MCP 的边界怎么判断

我的建议很简单:

  • 你自己业务内的小能力:直接定义 tool
  • 想复用标准化外部服务:优先接 MCP

Tool 是局部能力封装。
MCP 是跨进程、跨系统、可复用的能力接口。

它们不是互斥,而是同一层动作系统的两种接入方式。


判断五:Memory 和 RAG 解决的都不是“多存一点上下文”,而是控制模型看到什么

很多初学者容易把 memory 和 RAG 理解成“给模型加更多内容”。
这个理解太粗了。

它们真正解决的问题是:

当上下文有限时,哪些信息应该被带进这一轮推理。

这是一个上下文选择问题,而不是简单堆料问题。

为什么不能一直把所有消息都放进 messages

因为上下文窗口不是无限的。

对话一长,就会遇到这些问题:

  • token 成本快速升高
  • 旧信息淹没新信息
  • 有效信息比例下降
  • 最终直接超出模型限制

这也是为什么 Claude Code、Cursor 这类产品一旦上下文变长,就会开始做总结、压缩或选择性保留。

Memory 的 3 种策略,本质是 3 种上下文保留策略

1. 截断

最简单,保留最近几轮,丢掉更早的。

适合:

  • 临时会话
  • 近因信息更重要
  • 低成本场景

不适合:

  • 需要长期记住用户偏好
  • 老信息对当前问题仍然重要

2. 总结

把旧消息压缩成摘要,再带进上下文。

适合:

  • 长对话但细节不需要全部保留
  • 任务进展需要抽象状态而不是逐句回放

代价是:

  • 总结本身也要消耗模型调用
  • 信息一旦被总结,就会有损

3. 检索

把历史对话向量化,在当前 query 来时再召回相关部分。

这才是真正更接近“长时记忆”的方案。

因为它不是把所有历史都塞进来,而是按语义相关性选。

RAG 也是同一个问题,只不过对象从“历史消息”变成了“外部知识”

RAG 的核心流程可以压缩成两段:

  1. 离线准备知识
  2. 在线按 query 检索相关片段再回答

离线阶段通常包含:

  • Loader:从 PDF、网页、数据库、文件等加载内容
  • Splitter:把长文切成更适合向量化和召回的小块
  • Embedding:生成向量
  • 向量数据库入库

在线阶段通常包含:

  • 用户问题向量化
  • 相似度检索
  • 取回 Top K 片段
  • 拼进 Prompt
  • 让模型基于上下文回答

为什么 RAG 的关键不只是“向量库接上了”

因为实际效果受很多前置因素影响:

  • 文档切分是否合理
  • 块大小是否合适
  • overlap 是否足够
  • 元数据是否保留
  • 检索返回几条最合适
  • Prompt 是否清楚约束“只能基于资料回答”

比如 k 就不是越大越好。

  • 太小:容易漏召回
  • 太大:噪音片段太多,模型反而答偏

再比如 chunkSize 也没有绝对标准。

  • 太小:语义断裂
  • 太大:召回精度下降,还更占上下文

为什么长时记忆和 RAG 最终会靠向量数据库

因为无论对象是:

  • 企业知识库文档
  • 用户历史对话
  • 会话摘要
  • 外部数据片段

本质上都在回答同一个问题:

当前这个 query,最相关的上下文到底是什么?

只要问题是“按语义相关性挑上下文”,向量检索就是非常自然的手段。

所以我对 memory 和 RAG 的统一理解是:

  • memory:从“历史交互”里挑上下文
  • RAG:从“外部知识”里挑上下文

它们不是两门完全不同的技术,而是同一类上下文选择问题在不同数据源上的应用。

RAG 的标准流程:加载、切分、向量化、检索、再生成答案


判断六:真正把 LangChain 变成工程框架的,不是组件本身,而是 LCEL

如果只学前面的组件,LangChain 更像一套 AI 开发工具箱:

  • 这里一个 ChatModel
  • 那里一个 PromptTemplate
  • 再加个 OutputParser
  • 需要时绑个 Tool

这样当然能写功能,但一旦系统复杂,问题就来了:

  • 每个人组合方式都不一样
  • 调用顺序散在业务代码里
  • 想看某一步输入输出很麻烦
  • 想统一加重试、回调、埋点也很别扭

这就是为什么我认为 LCEL 是第一阶段里真正的分水岭。

学会组件,你会用 LangChain。
学会 LCEL,你才开始真正按工程方式写 LangChain。

LCEL 到底在做什么

LCEL 的底层是 Runnable 抽象。
很多组件都实现了 Runnable:

  • ChatModel
  • PromptTemplate
  • OutputParser
  • 自定义函数
  • 组合后的 chain

一旦它们都能被当成 Runnable,你就可以用统一方式把它们编排起来。

LCEL 通过 Runnable 把组件声明式组装成 chain

一条最简单的链

import { RunnableSequence } from "@langchain/core/runnables";

const chain = RunnableSequence.from([
  prompt,
  model,
  parser,
]);

const result = await chain.invoke(input);

这看起来像是“少写了几行代码”,但它真正改变的是程序结构:

  • 步骤不再散落,而是被声明成一条链
  • 整条链可以统一 invoke / stream / batch
  • 节点可以被动态增强

为什么声明式链路这么重要

因为 AI 系统最难维护的,通常不是单个节点,而是节点之间的关系。

比如这些典型场景:

  • 顺序处理:RunnableSequence
  • 条件分支:RunnableBranch
  • 并行计算:RunnableMap
  • 自定义转换:RunnableLambda
  • 保留原始输入:RunnablePassthrough
  • 带会话历史执行:RunnableWithMessageHistory

这套东西拼起来后,AI 应用才从“多个 API 调用”变成“一个工作流”。

LCEL 最强的地方,其实是节点级治理

原始资料里提到的几个能力,我认为都非常重要:

  • withRetry
  • withFallbacks
  • withConfig
  • callbacks

它们之所以关键,是因为它们说明了一件事:

LCEL 不只是把节点连起来,还允许你在节点上动态叠加治理能力。

这就很像工业流水线上的每一道工位:

  • 可以单独观测
  • 可以单独重试
  • 可以单独切换备选方案
  • 可以注入额外配置

一旦你理解到这一步,就会明白为什么后面的 LangSmith 会建立在 Runnable 和 callbacks 之上。
因为只有当链路被声明出来、节点边界清晰,监控和追踪才有抓手。

一个更接近工程实践的链路例子

const chain = RunnableSequence.from([
  normalizeQuestion,
  retrieveContext,
  buildPrompt,
  model,
  parser,
]).withRetry({
  stopAfterAttempt: 3,
});

这条链背后的工程意义比代码本身更重要:

  • 输入先归一化
  • 再检索知识
  • 再组装上下文
  • 然后调模型
  • 最后做结果解析

之后如果你想:

  • 流式输出:换成 stream
  • 批量处理:换成 batch
  • 观测每步耗时:加 callbacks
  • 检索失败重试:给 retriever 节点挂 withRetry

都不需要重写主流程结构。

这就是 LCEL 的价值。


这 6 个判断放在一起,LangChain 第一阶段真正学到的到底是什么

如果把这一阶段所有内容重新压缩,我会这样总结:

1. ChatModel 解决模型适配问题

它让你不用把业务逻辑写死在某一家模型的原生协议上。

2. PromptTemplate 解决上下文组织问题

它让 Prompt 从字符串,变成可维护的上下文组装层。

3. OutputParser 和 Structured Output 解决输出契约问题

它让模型结果能更稳定地进入业务系统,而不是停留在“看起来像对了”的自然语言层面。

4. Tool 和 MCP 解决动作执行问题

它让模型从“会回答”升级成“会决策并调用外部能力”。

5. Memory 和 RAG 解决上下文选择问题

它们都在回答:当前这一轮,到底该给模型看哪些信息。

6. LCEL 解决流程编排和治理问题

它把前面这些能力真正连接成一条可控、可观测、可扩展的工作流。

这 6 件事拼起来,才是一个完整 AI Agent 系统的骨架。


总结

学完 LangChain 第一阶段后,我最大的感受不是“又学会了几个库”,而是对 AI Agent 开发这件事有了更稳定的结构化理解。

过去很多人把 AI 应用理解成:

  • 写个 Prompt
  • 调个模型
  • 返回一段答案

但只要你真正做过一点项目,就会知道远不止如此。

你真正要解决的是这几个层面的问题:

  • 模型怎么解耦
  • 上下文怎么组织
  • 输出怎么约束
  • 外部动作怎么接入
  • 历史和知识怎么选择
  • 整条流程怎么编排和观测

LangChain 这套体系,正好对应了这几个层面。

所以我现在对 LangChain 的判断很简单:

只学组件时,它是一套 AI 工具集。
学到 LCEL 之后,它才真正变成一套 AI 应用工程框架。

这也是为什么我认为,第一阶段学完之后,最值得带走的不是 API 记忆,而是这套“把 AI 系统拆成输入、输出、动作、记忆、知识和流程”的工程认知。

后面继续学 LangGraph、LangSmith,或者自己做更复杂的 Agent,真正会反复用到的,也正是这套认知框架,而不是某一个具体类名。