第2周学习笔记
学习时间: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()这些方法签名,底层实现可以随意切换。
fromlangchain_openaiimportChatOpenAIfromlangchain_anthropicimportChatAnthropic# 两个不同厂商的模型,调用方式完全一致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 |
关键理解:
temperature和top_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 的基础。
三、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 | 自动按页拆分,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 “参数无效” 错误。
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())数据流分析:
RunnablePassthrough()将用户输入同时传给retriever(执行检索)和原样透传(保留原始问题)prompt模板将检索到的上下文注入 system 消息,原始问题注入 user 消息llm在上下文辅助下推理回答StrOutputParser提取纯文本
关键习惯:把上下文放在 system 消息中而非 user 消息中,让 system 承载"背景信息",user 保持用户的原始意图不被干扰。
五、Day 4:Agent 系统(上)——工具与 ReAct
5.1 工具定义:给模型装上手脚
工具是 Agent 系统中最基础的单元。@tool装饰器把一个普通 Python 函数自动转化为模型可理解的结构化接口:
@tooldefsearch_database(query:str,limit:int=10)->str:"""搜索客户数据库中的匹配记录。"""returnf"在数据库中找到了{limit}条与 '{query}' 相关的结果。"@tool做了三件事:类型标注 → JSON Schema;docstring → 用途描述;函数名 → 全局唯一工具名。
对于复杂参数场景,通过args_schema传入 Pydantic 模型可以获得更精细的控制:
classWeatherInput(BaseModel):city:str=Field(description="城市名称")units:Literal["celsius","fahrenheit"]=Field(default="celsius",description="温度单位")@tool(args_schema=WeatherInput)defget_weather(city:str,units:str="celsius")->str:"""获取指定城市的当前天气信息。"""...每个字段的Field(description=...)是模型决定如何填充参数的关键依据——描述模糊会导致模型在错误的情境下调用工具或填入不合理的参数。
5.2 ReAct 模式:推理与行动交替进行
这是 Agent 系统最经典的思想框架。它的执行流程是一条优雅的循环链条:
Question → Thought → Action → Observation → Thought → ... → Final Answer以一个具体场景为例:用户问"北京今天多少度?华氏度是多少?"
- Thought:模型意识到需要先获取温度 → 决定调用
get_weather - Action:调用
get_weather(city="北京") - Observation:收到 “北京:晴,25°C”
- Thought:模型判断还需要换算华氏度 → 决定调用
calculate - Action:调用
calculate(expression="25 * 9/5 + 32") - Observation:收到 “77.0”
- Final Answer:“北京今天 25°C,约 77°F”
ReAct 的美妙之处在于——把"推理"和"行动"统一为可迭代、可观测的过程。每一步思考都有文本记录,每一次工具调用都有可追溯的输入输出。
5.3 错误处理:当工具出错时
生产环境中的 Agent 不可能一帆风顺。@wrap_tool_call中间件像一个透明的拦截器,将工具异常转化为模型可理解的ToolMessage:
@wrap_tool_calldefhandle_tool_errors(request,handler):try:returnhandler(request)exceptExceptionase:returnToolMessage(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_agent、AgentExecutor、AgentAction中的功能,把所有复杂性封装进由 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_modeldefadd_timestamp(state:AgentState,runtime:Runtime):now=datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")state["messages"].append(HumanMessage(content=f"[系统信息] 当前时间:{now}"))returnNone中间件执行顺序遵循洋葱模型:before_*按列表正序执行,after_*按列表反序执行,wrap_*形成嵌套结构(列表前面的包裹在最外层)。
官方预置中间件覆盖了完整场景: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 生命周期。两者都通过"定义协议 + 自由组合"的方式实现了极高的扩展性。
八、踩坑记录与经验
check_embedding_ctx_length=False的重要性:使用硅基流动等非 OpenAI 服务商时,必须显式设置此参数为False。默认True会让OpenAIEmbeddings使用tiktoken将文本转换为 token ID 数组发送给 API,但硅基流动不支持 token 化输入,会返回 20015 “参数无效” 错误。这个问题 debug 了很久才定位到。langchain-community被标记为 deprecated:在使用langchain_community.document_loaders和langchain_community.vectorstores时,会收到 DeprecationWarning,提示该包正在被逐步淘汰。官方建议迁移到独立的集成包(如langchain-chroma、langchain-pdf等)。学习阶段不影响使用,但生产项目需要注意迁移路径。中文文档的 separators 必须定制:
RecursiveCharacterTextSplitter默认分隔符是英文导向的(\n\n,\n, ,"")。处理中文文档时,如果不在 separators 中加入。、,、;,分割器会跳过所有中文标点直接退化到空格或字符级切割,导致语义碎片化。VectorStore不是Runnable:向量存储对象不能直接用|接入管道,必须通过as_retriever()包装。这个细节容易忽略,直接写vectorstore | prompt会报类型错误。工具名应使用 snake_case:部分模型提供商会拒绝包含空格或特殊字符的函数名。
@tool装饰器默认使用函数名作为工具名,保持 Python 原生命名习惯即可。@before_model返回 None 的含义:当中间件通过修改state["messages"]原地生效后,返回None表示不需要额外更新 state。如果返回一个 dict,框架会用它来部分更新 state。
九、下周展望
第 3 周的主题是高级特性与生产级应用。从本周的认知出发:
- RAG 的检索质量可以通过多路检索融合、重排序(Re-ranking)、HyDE 假设文档嵌入等高级策略进一步提升
- Agent 的可靠性可以通过更多的中间件组合(如
SummarizationMiddleware自动压缩长对话历史、HumanInTheLoopMiddleware在关键操作前暂停审批)来保障 checkpointer+thread_id的多轮对话能力将在实际应用中发挥核心作用context_schema让 Agent 能感知用户身份、权限级别等运行时上下文,是构建多租户应用的基础
十、本周学习成果自检
- 理解
BaseLanguageModel的统一抽象设计,能对比不同模型在同一 prompt 下的输出差异 - 掌握 temperature / max_tokens / top_p / frequency_penalty 四个参数的作用与调优哲学
- 能使用
RunnableBranch实现基于任务特征的模型路由器 - 能使用至少 3 种文档加载器加载不同格式的文档
- 理解
RecursiveCharacterTextSplitter的递归降级分割机制,能为中文文档定制 separators - 掌握 embedding 模型的使用,理解
check_embedding_ctx_length参数的含义 - 能构建完整的 RAG Chain(Load → Split → Embed → Store → Retrieve → Generate)
- 理解三种检索策略(similarity / MMR / threshold)的原理与适用场景
- 能使用
@tool装饰器和args_schema定义结构化的工具接口 - 理解 ReAct 循环(Thought → Action → Observation)的运作机制
- 能使用
create_agent()构建完整的 Agent 系统 - 理解
checkpointer+thread_id实现多轮对话的原理 - 能编写自定义 middleware(
@before_model/@wrap_tool_call) - 理解 ReAct 与 Function Calling 的异同及
create_agent的自动适配策略
