Week 2 -- 第2周学习笔记

第 2 周学习笔记:LangChain 核心组件深度掌握

学习时间:2026年5月(5天) | 核心主题:Model I/O → RAG 系统构建 → Agent 与工具系统 → 中间件架构


一、本周学习路线总览

本周以"从单模型调用到构建完整的智能应用系统"为主线,按以下路径递进:

模型抽象与调优 → 文档加载与分割 → 向量嵌入与检索 → 工具定义与 ReAct → Agent 标准入口与中间件
    Day1              Day2              Day3              Day4                    Day5

如果说第 1 周是学会"如何用 LCEL 搭积木",那么第 2 周的核心命题是——如何让模型接入外部世界。RAG 给模型接上知识库,Agent 给模型接上工具,两者叠加让模型从"会说话的百科全书"进化为"能做事、能查资料的智能助手"。


二、Day 1:Model I/O 与模型抽象

2.1 BaseLanguageModel:一套接口,所有模型

本周第一个重要认知:LangChain 通过 BaseLanguageModel 将不同厂商的模型统一在同一套接口下。无论用 OpenAI、Anthropic,还是通过硅基流动接入的国产模型,上层的 Chain 和 Agent 只依赖 invoke() / stream() / batch() 这些方法签名,底层实现可以随意切换。

from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic

# 两个不同厂商的模型,调用方式完全一致
llm_openai = ChatOpenAI(model="gpt-5.5", temperature=0.1)
llm_claude = ChatAnthropic(model="claude-3-5-sonnet")

这种设计的哲学是面向接口编程:切换模型只需改一行 import 和初始化参数,管道的其余代码一行不动。这是第 1 周"协议胜于继承"思想在模型层的延续。

2.2 参数调优:四个旋钮的艺术

理解四个核心参数对模型行为的影响,是写出高质量应用的前提:

参数 作用 建议值
temperature 随机性控制 RAG: 0.0, 创作: 0.7-0.9
max_tokens 最大输出长度 按场景设定(成本控制 + 安全兜底)
top_p nucleus sampling 候选词截断 默认 1.0
frequency_penalty 抑制重复 需要时 0.1-0.3

关键理解:

  • temperaturetop_p 二选一调优为主,不建议同时大幅偏离默认值
  • temperature=0 在学习阶段尤为重要——确保每次运行结果可复现,调试时不会因为随机性而困惑
  • frequency_penalty 是解决模型"复读机"问题的利器

2.3 多模型路由:RunnableBranch 的实战应用

RunnableBranch 不仅是第 1 周学到的条件分支组件,它在模型路由场景中找到了最佳的用武之地——根据输入特征(文本长度、任务类型、复杂度)动态选择最合适的模型:

router = RunnableBranch(
    (is_long_context, llm_powerful),      # 长文本 → 强模型
    (is_code_task, llm_powerful),         # 代码任务 → 强模型
    (is_complex_question, llm_powerful),  # 复杂问题 → 强模型
    llm_fast                                # 默认 → 快速模型
)

RunnableBranch 本身也是 Runnable,可以用 | 接入任何 LCEL 管道——这让原本线性的管道具备了"分叉"能力,是构建非平凡 Agent 的基础。

2.4 四种调用方式:invoke / stream / batch / ainvoke

BaseLanguageModel(以及所有 Runnable)提供了四种标准调用方式,同一条 chain 无需修改即可在不同场景下切换:

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

对于模型调用而言,这四种方式同样适用——ChatOpenAI 作为 BaseChatModel 的实现类,天然支持全部四种调用模式。在构建生产级应用时,stream() 用于提升用户体验(逐字输出),batch() 用于批量评估和离线标注,ainvoke() 用于高并发 Web 服务。这种"一套接口,多种调用"的设计让同一段业务逻辑可以无缝适配不同的运行环境。


三、Day 2:RAG 系统构建(上)——文档加载与文本分割

3.1 RAG 六步流程

从今天开始进入 LangChain 最具工程价值的领域——RAG。一个完整的 RAG 系统可以概括为六个字:

Source → Load → Transform → Embed → Store → Retrieve
 源       载        转          嵌      存        检

Day 2 聚焦在前两步 Load 和 Transform,这是整个 RAG 系统的地基。

3.2 文档加载:统一接口下的多格式支持

langchain_community.document_loaders 提供了上百种加载器,覆盖 PDF、Markdown、CSV、HTML 等所有常见格式,共享同一套调用契约——load() 返回 List[Document]

加载器 用途 关键细节
TextLoader 纯文本 需要显式指定 encoding="utf-8"
PyPDFLoader PDF 自动按页拆分,metadata 含页码
UnstructuredMarkdownLoader Markdown 识别标题层级并写入 metadata
DirectoryLoader 批量加载 通过 glob 匹配文件,loader_cls 指定加载器

3.3 文本分割:RecursiveCharacterTextSplitter 的设计智慧

分割的核心矛盾:chunk 太大会丢失检索精确性,chunk 太小会割裂语义。RecursiveCharacterTextSplitter 通过递归降级分隔符的设计解决了这个问题:

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", ",", ";", " ", ""],
)

关键参数理解:

  • separators 的优先级顺序:先尝试段落边界 \n\n → 行边界 \n → 中文标点 。,; → 空格 → 最终兜底逐字符切割。中文文档必须加入中文标点,否则会退化到字符级切割。
  • chunk_overlap:相邻 chunk 之间的重叠部分(推荐值为 chunk_size 的 10%~20%),解决了"关键信息恰好落在切割边界上"的问题。
  • chunk_size:500 是工程上的常用起点,在精度和上下文之间取得平衡。

四、Day 3:RAG 系统构建(下)——嵌入、存储与检索

4.1 向量嵌入:让机器"理解"文本

向量嵌入的本质是将自然语言映射到高维向量空间,语义相近的文本在高维空间中几何上彼此靠近。LangChain 中所有 embedding 模型遵循统一的 Embeddings 接口,提供两个核心方法:

  • embed_documents(texts) — 批量嵌入待存储的文档
  • embed_query(text) — 嵌入用户查询

重要踩坑:使用硅基流动等非 OpenAI 服务商时,必须设置 check_embedding_ctx_length=False。默认 True 会让 OpenAIEmbeddings 使用 tiktoken 将文本转为 token ID 数组发送,但硅基流动不支持这种格式,会返回 20015 "参数无效" 错误。

嵌入维度的工程考量:不同 embedding 模型输出的向量维度差异巨大,直接影响存储开销、检索速度和语义精度:

维度 模型示例 存储占用(每向量) 检索速度 语义精度
384 BGE-Small ~1.5 KB 最快 中等
768 BGE-Base ~3 KB 良好
1024 BGE-Large ~4 KB 中等 很好
1536 OpenAI Ada-002 ~6 KB 中等 优秀
3072 OpenAI text-embedding-3-large ~12 KB 较慢 最佳

维度越高,语义表达能力越强,但存储成本线性增长,检索速度也相应下降。OpenAI 的 text-embedding-3 系列引入了 Matryoshka 截断特性——模型实际输出 3072 维向量,但可以通过 dimensions 参数截取前 N 维使用(如 dimensions=512),在几乎不损失太多精度的情况下大幅降低存储和计算开销。这种"训练高维、使用低维"的策略在实际工程中非常实用,是性能与成本之间的优雅权衡。

4.2 向量存储与检索策略

三种检索策略各有适用的场景:

策略 search_type 原理 适用场景
相似度 similarity(默认) 余弦相似度排序,返回 Top-K 通用场景
MMR mmr fetch_k 候选中做多样性筛选 避免结果同质化
阈值过滤 similarity_score_threshold 只返回相似度超过 score_threshold 的结果 生产环境质量兜底

VectorStore 本身不实现 Runnable 接口,需通过 as_retriever() 包装为 Retriever 对象后才能接入 LCEL 管道。这个设计体现了检索器是比向量存储更通用的抽象——无论数据来自向量库、搜索引擎还是外部 API,都可以统一封装。

4.3 完整 RAG Chain:一条管道串联全部环节

rag_chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

数据流分析:

  1. RunnablePassthrough() 将用户输入同时传给 retriever(执行检索)和原样透传(保留原始问题)
  2. prompt 模板将检索到的上下文注入 system 消息,原始问题注入 user 消息
  3. llm 在上下文辅助下推理回答
  4. StrOutputParser 提取纯文本

关键习惯:把上下文放在 system 消息中而非 user 消息中,让 system 承载"背景信息",user 保持用户的原始意图不被干扰。


五、Day 4:Agent 系统(上)——工具与 ReAct

5.1 工具定义:给模型装上手脚

工具是 Agent 系统中最基础的单元。@tool 装饰器把一个普通 Python 函数自动转化为模型可理解的结构化接口:

@tool
def search_database(query: str, limit: int = 10) -> str:
    """搜索客户数据库中的匹配记录。"""
    return f"在数据库中找到了 {limit} 条与 '{query}' 相关的结果。"

@tool 做了三件事:类型标注 → JSON Schema;docstring → 用途描述;函数名 → 全局唯一工具名。

对于复杂参数场景,通过 args_schema 传入 Pydantic 模型可以获得更精细的控制:

class WeatherInput(BaseModel):
    city: str = Field(description="城市名称")
    units: Literal["celsius", "fahrenheit"] = Field(default="celsius", description="温度单位")

@tool(args_schema=WeatherInput)
def get_weather(city: str, units: str = "celsius") -> str:
    """获取指定城市的当前天气信息。"""
    ...

每个字段的 Field(description=...) 是模型决定如何填充参数的关键依据——描述模糊会导致模型在错误的情境下调用工具或填入不合理的参数。

5.2 ReAct 模式:推理与行动交替进行

这是 Agent 系统最经典的思想框架。它的执行流程是一条优雅的循环链条:

Question → Thought → Action → Observation → Thought → ... → Final Answer

以一个具体场景为例:用户问"北京今天多少度?华氏度是多少?"

  1. Thought:模型意识到需要先获取温度 → 决定调用 get_weather
  2. Action:调用 get_weather(city="北京")
  3. Observation:收到 "北京:晴,25°C"
  4. Thought:模型判断还需要换算华氏度 → 决定调用 calculate
  5. Action:调用 calculate(expression="25 * 9/5 + 32")
  6. Observation:收到 "77.0"
  7. Final Answer:"北京今天 25°C,约 77°F"

ReAct 的美妙之处在于——把"推理"和"行动"统一为可迭代、可观测的过程。每一步思考都有文本记录,每一次工具调用都有可追溯的输入输出。

5.3 错误处理:当工具出错时

生产环境中的 Agent 不可能一帆风顺。@wrap_tool_call 中间件像一个透明的拦截器,将工具异常转化为模型可理解的 ToolMessage

@wrap_tool_call
def handle_tool_errors(request, handler):
    try:
        return handler(request)
    except Exception as e:
        return ToolMessage(
            content=f"工具执行出错,请检查输入参数后重试。错误详情:{e}",
            tool_call_id=request.tool_call["id"],
        )

这贯彻了 ReAct 的核心理念——把一切(包括失败)都转化为可推理的信息。模型收到错误消息后会像处理正常结果一样分析它,然后决定重试、换参数还是告知用户。


六、Day 5:Agent 系统(下)——Function Calling 与 create_agent

6.1 Function Calling:模型与工具的通信协议

bind_tools() 是连接模型和工具世界的桥梁。在 create_agent 内部,框架自动完成工具绑定——你不需要显式调用 bind_tools()

ReAct vs Function Calling 的本质区别:

维度 ReAct Function Calling
实现方式 提示词工程,模型输出"带格式的文本" 模型原生能力,输出结构化 JSON
解析方式 正则/解析器从文本中提取 确定性 JSON 解析
调用准确率 依赖提示词质量,有格式偏差风险 通常是训练内置能力,更稳定
适用场景 无原生 FC 支持的模型;学术追溯思考过程 有 FC 支持的模型(生产首选)

在 LangChain v1 中,create_agent 自动根据模型能力选择最优策略——支持 tool calling 就走 Function Calling 路径,否则退回文本推理模式。你不需要改变构建代码。

6.2 create_agent:LangChain v1 的 Agent 标准入口

create_agent 统一了过去分散在 create_react_agentAgentExecutorAgentAction 中的功能,把所有复杂性封装进由 LangGraph 驱动的高层抽象。核心参数:

参数 作用
model 模型标识字符串或已初始化的聊天模型实例
tools 工具列表,框架自动完成绑定和注册
system_prompt Agent 的行为基调
response_format Pydantic 模型约束最终输出格式
checkpointer 持久化对话状态(InMemorySaver 用于开发,PostgresSaver 用于生产)
context_schema 定义每次调用的不可变上下文数据类型
middleware 可插拔的中间件列表

checkpointer + thread_id 是实现多轮对话的关键:

agent = create_agent(model=llm, tools=[...], checkpointer=InMemorySaver())

# 同一 thread_id 下的多次调用自动累积对话历史
result_1 = agent.invoke(
    {"messages": [{"role": "user", "content": "北京天气怎么样?"}]},
    config={"configurable": {"thread_id": "conversation-001"}}
)
result_2 = agent.invoke(
    {"messages": [{"role": "user", "content": "我刚才问了什么?"}]},
    config={"configurable": {"thread_id": "conversation-001"}}  # 同一个 thread_id
)
# Agent 能记住之前的对话!

6.3 Middleware:Agent 的可插拔扩展层

这是 create_agent 最令人惊艳的设计。中间件分为节点式和包裹式两种风格:

节点式钩子(在特定时间点运行):

  • @before_agent — Agent 调用开始前(全局状态初始化)
  • @before_model — 每次模型调用前(注入动态上下文,如当前时间戳)
  • @after_model — 每次模型响应后(日志记录、响应校验)
  • @after_agent — Agent 调用结束后(清理、汇总)

包裹式钩子(环绕调用,控制执行零次/一次/多次):

  • @wrap_model_call — 环绕模型调用(重试、缓存、动态切换模型)
  • @wrap_tool_call — 环绕工具调用(错误转换、日志、限流)

一个经典的 @before_model 中间件——为 Agent 注入时间感知能力:

@before_model
def add_timestamp(state: AgentState, runtime: Runtime):
    now = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")
    state["messages"].append(HumanMessage(content=f"[系统信息] 当前时间:{now}"))
    return None

中间件执行顺序遵循洋葱模型,理解这套规则对于设计复杂的中间件组合至关重要:

  • before_* 钩子按中间件列表的正序执行(从前到后)
  • after_* 钩子按中间件列表的反序执行(从后到前,类似洋葱模型的 return 阶段)
  • wrap_* 钩子形成嵌套结构,列表中排在最前面的中间件包裹在最外层(最先进入、最后退出)

middleware=[log_middleware, error_middleware, limit_middleware] 为例:limit_middleware 包裹 error_middlewareerror_middleware 包裹 log_middleware。before 钩子按 日志 → 错误 → 限流 的顺序触发,after 钩子按 限流 → 错误 → 日志 的顺序触发。官方文档推荐将关键的中间件放在列表前面(如错误处理、限流),因为它们会获得最外层的控制权。

官方预置中间件覆盖了完整场景:ModelRetryMiddleware(模型重试)、ToolRetryMiddleware(工具重试)、ModelFallbackMiddleware(模型降级)、HumanInTheLoopMiddleware(人工审批)、SummarizationMiddleware(对话历史压缩)、TodoListMiddleware(任务规划追踪)等。


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

7.1 对"文档 → 向量 → 检索 → 生成"管道的感悟

RAG 本质上就是在 LLM 管道的前端插入了一个检索步骤:retriever | prompt | llm | parser。第 1 周学到的 LCEL 知识在这里被完整复用——管道的可组合性让"插入新组件"极其自然。这印证了一个设计理念:好的抽象能经得起场景扩展的考验

7.2 对 Agent = Model + Harness 的理解

create_agent 的官方定义 "Agent = Model + Harness" 精准概括了 Agent 的本质。Model 负责思考(推理是否调用工具、如何填充参数、何时给出最终答案),Harness(包括工具绑定、状态管理、中间件、循环控制)负责为模型提供正确的上下文和行动框架。这种分离让 Agent 的"大脑"和"身体"可以独立演进而互不影响。

7.3 ReAct 与 Function Calling 的关系

两者不是"替代"关系,而是分层关系。ReAct 定义的是逻辑范式(推理-行动-观察的循环),Function Calling 提供的是底层实现(用结构化 JSON 替代文本解析)。在 LangChain v1 中,create_agent 为上层提供了统一的抽象,底层的选择只影响执行效率而不影响功能接口——这正是优秀的框架设计。

7.4 Middleware 与 Runnable 的设计共性

中间件系统和 LCEL 的 Runnable 接口共享同一个设计哲学:协议胜于继承Runnable 只要实现 invoke 就能接入管道,Middleware 只要实现 @before_model@wrap_tool_call 就能插入 Agent 生命周期。两者都通过"定义协议 + 自由组合"的方式实现了极高的扩展性。


八、踩坑记录与经验

  1. check_embedding_ctx_length=False 的重要性:使用硅基流动等非 OpenAI 服务商时,必须显式设置此参数为 False。默认 True 会让 OpenAIEmbeddings 使用 tiktoken 将文本转换为 token ID 数组发送给 API,但硅基流动不支持 token 化输入,会返回 20015 "参数无效" 错误。这个问题 debug 了很久才定位到。

  2. langchain-community 被标记为 deprecated:在使用 langchain_community.document_loaderslangchain_community.vectorstores 时,会收到 DeprecationWarning,提示该包正在被逐步淘汰。官方建议迁移到独立的集成包(如 langchain-chromalangchain-pdf 等)。学习阶段不影响使用,但生产项目需要注意迁移路径。

  3. 中文文档的 separators 必须定制RecursiveCharacterTextSplitter 默认分隔符是英文导向的(\n\n, \n, , "")。处理中文文档时,如果不在 separators 中加入 ,分割器会跳过所有中文标点直接退化到空格或字符级切割,导致语义碎片化。

  4. VectorStore 不是 Runnable:向量存储对象不能直接用 | 接入管道,必须通过 as_retriever() 包装。这个细节容易忽略,直接写 vectorstore | prompt 会报类型错误。

  5. 工具名应使用 snake_case:部分模型提供商会拒绝包含空格或特殊字符的函数名。@tool 装饰器默认使用函数名作为工具名,保持 Python 原生命名习惯即可。

  6. @before_model 返回 None 的含义:当中间件通过修改 state["messages"] 原地生效后,返回 None 表示不需要额外更新 state。如果返回一个 dict,框架会用它来部分更新 state。

  7. 多中间件的洋葱模型执行顺序:同时使用多个中间件时,before_* 按列表正序执行,after_* 按反序执行,wrap_* 形成嵌套结构。将最关键的中间件(如错误处理、限流)放在列表前面可以确保它们获得最外层的控制权。理解这个顺序对于设计复杂的中间件组合至关重要。

  8. RunnableBranch 默认分支陷阱RunnableBranch 的最后一个参数是默认分支,不需要条件函数。如果忘记提供默认分支且所有条件都不满足,会抛出异常。始终确保有一个兜底分支来处理未预见的输入情况。

  9. VectorStore 持久化路径的细节:使用 Chroma 时,persist_directory 指定的是目录路径而非文件路径。如果不设置此参数,数据只存在于内存中,程序退出后丢失。设置了路径后,下次启动时用相同路径初始化 Chroma 实例即可恢复全部数据,无需重新嵌入。

  10. Document.metadata 的来源追踪价值:在文档加载和分割过程中,metadata(源文件名、页码等)会随 chunk 一起存入向量数据库。检索时返回的 Document 对象仍然携带这些信息,可直接用于答案的引用溯源。在构建生产级 RAG 时,务必在 metadata 中保留足够的溯源信息。

  11. 旧版 Agent API 的废弃create_react_agent + AgentExecutor 的组合已在 LangChain v1 中被标记为 deprecated。新版 create_agent 通过中间件系统替代了 AgentExecutormax_iterationsverbosehandle_parsing_errors 等参数。如果参考旧教程学习,需要注意 API 的迁移路径。


九、下周展望

第 3 周的主题是高级特性与生产级应用。从本周的认知出发:

  • RAG 的检索质量可以通过多路检索融合、重排序(Re-ranking)、HyDE 假设文档嵌入等高级策略进一步提升
  • Agent 的可靠性可以通过更多的中间件组合(如 SummarizationMiddleware 自动压缩长对话历史、HumanInTheLoopMiddleware 在关键操作前暂停审批)来保障
  • checkpointer + thread_id 的多轮对话能力将在实际应用中发挥核心作用
  • context_schema 让 Agent 能感知用户身份、权限级别等运行时上下文,是构建多租户应用的基础

十、本周学习成果自检

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