Week 1 -- 第1周学习笔记

第 1 周学习笔记:LangChain 基础入门与架构理解

学习时间:2026年5月(5天) | 核心主题:生态全景 → 环境搭建 → LCEL → 提示工程与输出解析 → 综合实战


一、本周学习路线总览

本周以"从零到一构建可运行的 LLM 应用"为主线,按以下路径递进:

生态认知 → 环境落地 → 核心范式(LCEL) → 输入输出工程 → 综合整合
  Day1        Day2          Day3           Day4          Day5

每一天都建立在前一天的基础之上,最终在 Day 5 用一个多语言翻译助手项目将全部知识点串联起来。


二、Day 1:LangChain 生态全景

2.1 核心认知:LangChain 不是"一个库"

最大的认知转变是:LangChain 不是一个单体框架,而是一个分层生态。它由四个独立发布的包构成,每一层有明确的职责边界:

层级 包名 一句话定位
基础抽象层 langchain-core 定义"语法"——Runnable 接口 + LCEL
第三方集成层 langchain-community 数百个外部服务的统一适配器
业务组件层 langchain 开箱即用的 Chains / Agents / Retrieval
编排调度层 langgraph 图结构驱动的有状态工作流引擎

这种分层设计的关键收益是独立迭代langchain-core 的接口变更不会波及 langgraphlanggraph 也可以按自己的节奏发版。各层之间通过统一的 Runnable 接口通信,形成松耦合的体系。

2.2 2026 年 v1.2.14 的关键信号

这个版本释放了几个重要信号:

  • LangGraph 深度整合:不再是可选扩展,而是框架的原生组成部分 → 说明"有状态 Agent 工作流"已成为 LangChain 的核心场景
  • MCP 协议支持:跨框架工具互操作 → LangChain 在主动打破生态壁垒,一套工具定义可被多个 AI 框架复用
  • create_agent() 标准入口:降低 Agent 构建门槛 → 框架在从"灵活但复杂"向"简单且强大"演进

2.3 生态工具定位(易混淆点)

这三个工具经常被混淆,它们的本质区别是:

  • LangSmith → 解决"应用质量"问题:调试追踪、评估测试、生产部署监控
  • LangGraph → 解决"流程复杂度"问题:多步骤、有状态、带分支的工作流编排
  • Deep Agents → 解决"任务复杂度"问题:长周期规划、多子 Agent 协作

三、Day 2:环境搭建与第一个应用

3.1 环境搭建的关键细节

实际踩过的坑:

  1. 包的独立性langchain-corelangchain 的依赖,会自动安装;但 langgraphlangchain-openai 需要单独安装,因为它们是完全独立的项目。
  2. 模型兼容性:本次学习统一使用硅基流动 + langchain-openai,因为市面上绝大多数模型(DeepSeek、Qwen、Ollama 本地模型等)都兼容 OpenAI API 格式,一个包覆盖几乎所有模型
  3. 密钥安全.env + .gitignore 是最低成本的防泄露方案。注意 load_dotenv() 必须在代码最顶部调用,且 OPENAI_API_KEY 这个变量名是 LangChain 的默认约定。

3.2 第一个 LCEL 链的拆解

chain = prompt | llm | output_parser
result = chain.invoke({"role": "Python专家", "question": "解释装饰器"})

这条链实际上做了三件事:

  1. prompt:将 {role}{question} 替换为实际值,生成格式化的消息列表
  2. llm:将消息列表发送给大模型 API,返回 AIMessage 对象
  3. output_parser:从 AIMessage 中提取 .content,返回纯字符串

关键感悟:LangChain 的价值不在"能做什么"(裸调 API 也能做),而在"怎么做"(声明式组合、统一接口、关注点分离)。当需要切换模型时,只改一行初始化代码,chain 的其余部分完全不变——这就是接口抽象的意义。

3.3 对比裸调 API 的优势总结(<100字)

LangChain 将"拼字符串→调 API→解析结果"的散落逻辑收敛为声明式管道,切换模型只需改一行;Prompt 模板可复用可测试,输出解析自动处理格式差异,工程化程度显著高于裸调。


四、Day 3:LCEL 深度理解(本周最核心)

4.1 Runnable:LangChain 的"通用语言"

这是本周最重要的概念。LangChain 中几乎所有组件(Prompt 模板、LLM、Parser、Retriever、Tool……)都实现了 Runnable 接口。这意味着它们在 LCEL 管道中是等价的积木,可以任意拼接。

Runnable 提供三种标准调用方式,同一条 chain 无需修改即可切换:

方法 场景 特点
invoke() 单次请求 同步阻塞,返回完整结果
stream() 对话类应用 逐 token 迭代输出,实时反馈
batch() 批量离线处理 内部并发执行,比循环 invoke

4.2 管道运算符 | 的本质

| 在 Python 中是位运算符,LangChain 通过重载 __or__ 将其变成组合运算符step1 | step2 等价于 RunnableSequence(step1, step2)

最关键的特性是惰性求值

chain = prompt | llm | parser   # ← 此时没有任何 API 调用
result = chain.invoke(...)       # ← 真正的执行发生在这里

这种设计带来的工程收益:

  • 构建与执行分离:链可以被序列化、缓存、跨函数传递
  • 配置即代码:链的定义本身就是一个可版本管理的配置对象
  • 复用性强:同一条链可以被多次 invoke 而不需要重新构建

4.3 RunnableParallel:并发分发的利器

当同一份输入需要经过多条独立分析路径时,RunnableParallel 让这些路径并发执行,显著减少总等待时间。

parallel = RunnableParallel(
    summary=summary_chain,
    keywords=keywords_chain,
    sentiment=sentiment_chain,
)
result = parallel.invoke({"article": "..."})
# result = {"summary": "...", "keywords": "...", "sentiment": "..."}

实际验证:三路分析的总耗时约等于最慢那一路的耗时,而非三路之和。

4.4 RunnableBranch:动态路由

本质是 LCEL 版的 if/elif/else,根据输入的谓词判断决定走哪条处理路径:

branch = RunnableBranch(
    (lambda x: "紧急" in x["topic"], urgent_chain),
    normal_chain,  # 默认分支
)

RunnableBranch 本身也是 Runnable,可以嵌套进更大的 chain 中。

4.5 RunnableLambda:连接自定义逻辑的桥梁

任意 Python 函数包装成 RunnableLambda 后就可以接入 LCEL 管道:

postprocess = RunnableLambda(lambda text: f"[PROCESSED] {text.upper()}")
chain = prompt | llm | parser | postprocess

这让 LCEL 的边界可以无限扩展——任何业务逻辑,只要写成函数,就能无缝融入管道。


五、Day 4:提示工程与输出解析

5.1 Prompt 模板体系

ChatPromptTemplate.from_messages 支持三种消息类型:

类型 写法 用途
角色消息 ("system", "你是{role}") 定义 AI 的行为规范
用户消息 ("user", "{input}") 承载用户输入
消息插槽 MessagesPlaceholder(variable_name="history") 动态注入历史消息列表

5.2 MessagesPlaceholder 的本质区别(关键理解)

这是我本周最重要的理解之一。MessagesPlaceholder 和普通花括号占位符 {variable} 的本质区别在于接受的值的类型

  • {role} 接受的是单个字符串 → 渲染时直接替换文本
  • MessagesPlaceholder("history") 接受的是消息对象列表 → 渲染时将列表中的每条消息展开插入

这个区别决定了多轮对话的实现方式:你不需要手动拼接历史文本,只需维护一个 [HumanMessage, AIMessage, HumanMessage, AIMessage, ...] 列表并传入即可,LangChain 自动完成展开和排序。

手动维护历史的做法

history = []
# 第一轮
result1 = chain.invoke({"history": history, "input": "问题1"})
# 第二轮:将上一轮的问答追加到 history
history = [HumanMessage("问题1"), AIMessage(result1)]
result2 = chain.invoke({"history": history, "input": "问题2"})

5.3 Few-Shot 提示

核心思想:用示例替代冗长的文字描述。3~5 个精心挑选的输入输出对,往往比一大段格式说明更能让模型理解期望。

使用步骤:

  1. 准备 examples 列表(每个元素是 {"input": ..., "output": ...} 字典)
  2. FewShotChatMessagePromptTemplate 包装示例
  3. few_shot_prompt 嵌入主 prompt 模板中

5.4 输出解析器对比

解析器 返回类型 类型安全 适用场景
StrOutputParser str 纯文本回答
JsonOutputParser dict 简单结构化提取
PydanticOutputParser Pydantic 实例 需要类型校验的生产场景

推荐在工程中优先使用 PydanticOutputParser,因为:

  • Pydantic 自动做类型强制(如 "28"28
  • IDE 中获得完整的类型提示和自动补全
  • 字段缺失或类型错误时抛出明确的校验异常

5.5 OutputParserException 与降级策略

LLM 并不总是听话的。即使 prompt 中明确要求输出 JSON,模型有时也会在 JSON 前后附加额外文字。正确的工程实践是:

try:
    result = chain.invoke({"input": user_input})
except OutputParserException as e:
    # 降级:回退到纯文本,让下游自行处理
    fallback_result = fallback_chain.invoke(...)

永远不要假设解析一定成功,这是从 Demo 走向生产的关键一课。


六、Day 5:综合项目——多语言翻译助手

6.1 项目架构

用户输入 → Prompt 模板 → LLM (Qwen3.6-35B) → PydanticOutputParser → TranslationResult
                ↑                                    ↓ (异常时)
           format_instructions               OutputParserException → 降级处理

6.2 技术点映射

本周知识点 在项目中的体现
LCEL 管道 chain = prompt | llm | parser
PydanticOutputParser TranslationResult 的结构化输出 + 类型校验
prompt.partial() format_instructions 预先绑定到模板
OutputParserException try-except 降级处理
ChatPromptTemplate.from_messages system + user 双消息结构
Field(description=...) 字段描述自动生成 JSON schema 格式说明

6.3 关键代码片段

class TranslationResult(BaseModel):
    source_lang: str = Field(description="源语言,如'中文'或'英文'")
    target_lang: str = Field(description="目标语言")
    original: str = Field(description="用户输入的原文")
    translated: str = Field(description="翻译后的文本")
    confidence: float = Field(description="翻译置信度,0.0~1.0")

parser = PydanticOutputParser(pydantic_object=TranslationResult)
prompt = prompt.partial(format_instructions=parser.get_format_instructions())
chain = prompt | llm | parser

这个项目不到 100 行代码,但已经是一个有工程价值的 LLM 应用原型:结构化输出、类型安全、异常处理、可扩展——这些都是在"裸调 API"时需要额外编写大量代码才能获得的特性。


七、核心概念的个性化理解

7.1 对 LCEL "声明式组合"的理解

传统命令式写法关心的是怎么做(每一步如何调用、传什么参数、怎么处理结果),而 LCEL 关心的是数据流向(先经过 A,再经过 B,最后到 C)。prompt | llm | parser 读起来就像"输入 → 格式化 → 推理 → 解析 → 输出",代码结构本身就是流程图。

7.2 对 Runnable 接口的感悟

Runnable 是 LangChain 的"通用货币"。它让我想起 Python 的迭代器协议——只要实现了 __iter__,任何对象都可以被 for 循环消费。同样的,只要实现了 Runnable,任何组件都可以被 | 组合。这是一种协议胜于继承的设计哲学。

7.3 对提示工程 + 输出解析的认识

如果把 LLM 比作一个"黑盒函数",那么:

  • Prompt 模板是函数的参数定义和类型注解(告诉模型"你该接收什么输入、扮演什么角色")
  • Output Parser是函数的返回值类型约束(告诉模型"你该输出什么格式,我会这样校验你")
  • 两者配合,让不可控的自然语言输入输出变得可预测、可校验、可工程化

八、踩坑记录与经验

  1. Field(...) 中的 ...(Ellipsis)vs 默认值:定义 Pydantic 模型时,必需字段用 Field(...)(Ellipsis 表示 required),可选字段用 Field(default=...)。一开始漏写导致字段都变成可选,解析时没有报错但数据不完整。

  2. base_url 参数:使用硅基流动等第三方 API 时,必须显式传入 base_url="https://api.siliconflow.cn/v1",否则 ChatOpenAI 默认请求 OpenAI 官方服务器。

  3. temperature=0 对学习阶段的重要性:非零温度会让模型每次输出不同,导致调试时行为不一致。学习阶段设为 0 可以确保每次运行结果可复现。

  4. prompt.partial() 的使用场景:一些不随每次调用改变的参数(如 format_instructions)应该用 partial 预绑定,这样调用时只需要传业务变量,接口更清晰。


九、下周展望

第 2 周的主题是 RAG(检索增强生成)。从本周的认知出发,RAG 在 LCEL 架构中本质上就是:

retriever | prompt | llm | parser
    ↑
  在管道前端插入一个检索步骤

这意味着本周学到的所有 LCEL 知识在下周可以直接复用——管道的可组合性让"插入新组件"变得极其简单。同时,MessagesPlaceholder 管理对话历史的能力在 RAG 的多轮检索场景中也会发挥关键作用。


十、本周学习成果自检

posted @ 2026-07-04 23:49  喵叔哟  阅读(2)  评论(0)    收藏  举报