# LangChain-Learn **Repository Path**: vpen/lang-chain-learn ## Basic Information - **Project Name**: LangChain-Learn - **Description**: langchain学习,包含各种示例 - **Primary Language**: Unknown - **License**: GPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-05-28 - **Last Updated**: 2026-05-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # LangChain-Learn 这是一个专门给 LangChain 初学者准备的小项目。 这个仓库的目标不是一上来就堆很多高级概念, 而是尽量用短小、能跑、能读懂的示例, 帮你一步一步建立对 LangChain 的基本理解。 如果你现在还是初学者, 最重要的不是把所有名词都背下来, 而是先看懂这条主线: `输入 -> Prompt -> Model -> 输出` 后面的 Structured Output、Workflow、Conversation History、RAG、Agent, 本质上都是围绕这条主线继续加能力。 ## 这个项目适合谁 适合下面这类同学: - 会一点 Python,但刚开始接触大模型应用开发 - 听过 LangChain,但还不清楚它到底解决什么问题 - 看网上教程时,经常会被 Agent、RAG、Tool、Workflow、Memory 这些词绕晕 - 希望先从“能跑的最小示例”建立理解 - 希望代码里有比较细、比较通俗的中文注释 ## 你会学到什么 - `ChatOpenAI` 在 LangChain 里扮演什么角色 - OpenAI Responses API 和 Chat Completions API 的输入输出有什么不同 - LangChain 上层封装和 OpenAI 底层 API 之间是什么关系 - `invoke()` 为什么几乎到处都能看到 - `ChatPromptTemplate` 到底在帮你做什么 - `MessagesPlaceholder` 为什么能把历史消息插进 Prompt - `StrOutputParser` 为什么有用 - `prompt | model | parser` 这条链是什么意思 - 结构化输出为什么比长文本更适合程序处理 - 多步骤 Workflow 和 Agent 的区别 - 多轮对话为什么能接住上一轮上下文 - 为什么对话越来越长时,需要给历史消息加”窗口” - `invoke()` 和 `stream()` 的区别,流式输出怎么实现 - 怎么用 `with_retry()` 和 `with_fallbacks()` 让系统更健壮 - 怎么用 Map-Reduce 模式处理长文档 - `batch()` 和 `for + invoke()` 的性能差异 - 怎么用不同 Loader 加载 .txt、.csv、.json 等格式 - 为什么要把长文档切成小块,两种切分器有什么区别 - Embedding 把文本变成向量后,怎么用余弦相似度比较语义 - `RunnablePassthrough` 和 `RunnableParallel` 在链里怎么流动数据 - 怎么在 Prompt 里给模型”例题”来提高输出质量(Few-Shot) - 为什么向量检索的 top-K 不一定最相关,怎么做 Reranking - 服务端 Prompt Caching 和 LangChain 本地缓存有什么区别 - 怎么从返回结果里看 `cached_tokens` / `cache_read` - RAG 到底是在做”先查资料,再回答”这件事 - metadata 和 source 在 RAG 里为什么重要 - Tool 是什么,为什么 Agent 会调用 Tool - 怎么用”生成 → 评审 → 改进”的反思循环提高输出质量 ## 项目结构 ```text LangChain-Learn/ ├── .env.example ├── README.md ├── requirements.txt ├── data/ │ ├── langchain_notes.md │ └── rag_demo_docs/ │ ├── 01_chain_and_workflow.md │ ├── 02_rag_basics.md │ ├── 03_agent_basics.md │ └── 04_conversation_history.md └── examples/ ├── 00_direct_model_call.py ├── 01_prompt_chain.py ├── 02_agent_with_tools.py ├── 03_structured_output.py ├── 04_simple_rag.py ├── 05_multi_step_workflow.py ├── 06_conversation_history.py ├── 07_multi_file_rag.py ├── 08_server_side_prompt_cache.py ├── 09_responses_vs_chat_completions.py ├── 10_serial_loop_chat.py ├── 11_chain_step_by_step.py ├── 12_query_rewrite_rag.py ├── 13_multi_tool_agent.py ├── 14_hybrid_search_rag.py ├── 15_agent_tool_router.py ├── 16_history_window_memory.py ├── 17_reflection_agent_loop.py ├── 18_streaming_output.py ├── 19_fallback_and_retry.py ├── 20_summarize_map_reduce.py ├── 21_batch_invoke.py ├── 22_document_loader.py ├── 23_text_splitters.py ├── 24_embeddings_demo.py ├── 25_runnable_passthrough.py ├── 26_few_shot_prompting.py └── 27_reranking.py ``` ## 推荐学习顺序 虽然文件名里有 `00 ~ 27`, 但如果你是初学者,更推荐按下面这个顺序看: 1. `examples/00_direct_model_call.py` 2. `examples/09_responses_vs_chat_completions.py` 3. `examples/01_prompt_chain.py` 4. `examples/11_chain_step_by_step.py` 5. `examples/03_structured_output.py` 6. `examples/05_multi_step_workflow.py` 7. `examples/06_conversation_history.py` 8. `examples/16_history_window_memory.py` 9. `examples/18_streaming_output.py` 10. `examples/04_simple_rag.py` 11. `examples/22_document_loader.py` 12. `examples/23_text_splitters.py` 13. `examples/24_embeddings_demo.py` 14. `examples/07_multi_file_rag.py` 15. `examples/12_query_rewrite_rag.py` 16. `examples/14_hybrid_search_rag.py` 17. `examples/27_reranking.py` 18. `examples/25_runnable_passthrough.py` 19. `examples/26_few_shot_prompting.py` 20. `examples/02_agent_with_tools.py` 21. `examples/13_multi_tool_agent.py` 22. `examples/15_agent_tool_router.py` 23. `examples/17_reflection_agent_loop.py` 可选补充: - `examples/08_server_side_prompt_cache.py` - `examples/10_serial_loop_chat.py` - `examples/19_fallback_and_retry.py` - `examples/20_summarize_map_reduce.py` - `examples/21_batch_invoke.py` ### 为什么这样排 因为这是从“最基础”到“更灵活”的理解顺序。 ### 第一步:先看 `00_direct_model_call.py` 这一步只做一件事: 让你看懂最基础的 `model.invoke(...)`。 很多人一上来就看 `prompt | model | parser`, 结果连“模型到底是怎么被调用的”都还没建立感觉。 所以这个示例是整个仓库的新起点。 ### 第二步:插读 `09_responses_vs_chat_completions.py` 这一步不是 LangChain 示例, 而是直接使用 OpenAI Python SDK 调用两个底层 API: - `client.responses.create(...)` - `client.chat.completions.create(...)` 为什么建议放在这里看? 因为很多初学者会把三件事混在一起: - OpenAI API:模型服务本身提供的接口 - Chat Completions API:传统聊天接口 - LangChain:把模型调用、Prompt、Parser、RAG、Tool 串起来的上层框架 看完 09 以后, 你再回来看 `ChatOpenAI(...)`, 就更容易理解它是在帮你封装底层模型调用。 ### 第三步:再看 `01_prompt_chain.py` 这一步开始引入最经典的 LangChain 写法: ```python chain = prompt | model | parser ``` 你会开始理解: LangChain 不是平白无故发明新语法, 而是在把“准备输入、调用模型、整理输出”这几步串起来。 如果你对“为什么同样一类长 Prompt,后面几次调用可能更快、更省”感兴趣, 这时也可以插读 `examples/08_server_side_prompt_cache.py`。 因为它本质上还是 Prompt + Model, 只是多加了一个“服务端缓存优化”的观察点。 ### 第四步:插读 `11_chain_step_by_step.py` 如果你看完 `01_prompt_chain.py` 以后, 还是觉得 `prompt | model | parser` 有点抽象, 那就马上看 11。 这个示例会把一条链拆成下面 4 个显式步骤: 1. 准备输入变量 2. 调用 `prompt.invoke(...)` 填好模板 3. 调用 `model.invoke(...)` 真正请求模型 4. 调用 `parser.invoke(...)` 把结果整理成字符串 对初学者来说,这一步非常有价值, 因为它能把“一行链式写法”还原成你已经熟悉的普通 Python 步骤。 ### 第五步:再看 `03_structured_output.py` 这一步会告诉你: 模型不一定只会返回长文本, 它也可以被要求输出固定字段。 这是很多实际业务里非常重要的能力。 ### 第六步:再看 `05_multi_step_workflow.py` 这一步非常关键。 很多初学者一学到 Tool 或 Agent, 就会误以为“只要流程复杂一点,就必须上 Agent”。 这个示例会告诉你: 很多场景里,其实普通 Python + 多条小 Chain 就够了。 ### 第七步:再看 `06_conversation_history.py` 这一步会告诉你: 多轮对话很多时候不是“模型神奇记住了你”, 而是程序每一轮都把历史消息重新传了进去。 这对理解聊天应用非常重要。 ### 第八步:再看 `04_simple_rag.py` 这一步进入最小 RAG。 你会看到: RAG 不是一个神秘黑盒, 它本质上只是“先检索资料,再基于资料回答”。 ### 第九步:再看 `07_multi_file_rag.py` 07 可以看成 04 的自然下一步。 它会告诉你: RAG 的资料源不一定只有一个文件, 你完全可以把一整个目录里的多份资料一起做成小知识库。 ### 第十步:再看 `12_query_rewrite_rag.py` 当你已经理解了“RAG 是先检索再回答”以后, 下一步很值得学的就是: 用户原问题不一定直接适合检索。 12 会告诉你: - 为什么很多系统会先做 Query Rewrite - 为什么“口语化问题”有时要先改成“更适合搜索的查询” - 为什么改写的目标不是直接回答用户,而是帮 Retriever 更容易找到资料 ### 第十一步:再看 `14_hybrid_search_rag.py` 当你已经知道: - 向量检索能按语义找资料 - Query Rewrite 能先把问题改写得更适合检索 下一步很值得学的就是: 为什么很多系统不会只用一种检索方式, 而是把“关键词检索 + 向量检索”结合起来。 14 会告诉你: - 关键词检索更像按字面去找 - 向量检索更像按意思去找 - 混合检索不是二选一,而是两边都找,再合并结果 ### 第十二步:再看 `02_agent_with_tools.py` Agent 是这个仓库里理解门槛相对更高的示例。 它的重点不是“如何固定执行某条流程”, 而是“让模型自己判断要不要调用工具、调用哪个工具”。 所以更适合你在前面几个概念已经稳住以后再看。 ### 第十三步:再看 `13_multi_tool_agent.py` 13 可以看成 02 的进阶版。 它会告诉你: - 一个 Agent 不一定只有一个工具 - 不同工具可以负责不同类型的信息源 - Agent 可能会为了回答一个问题,连续调用多个工具 ### 第十四步:最后看 `15_agent_tool_router.py` 15 可以看成 13 的下一步。 它会告诉你: - 工具很多时,不一定要全部直接暴露给 Agent - 可以先做一次路由,再只开放本次最相关的一小组工具 - 有些问题甚至根本不需要先上 Agent,直接回答反而更简单 ### 可选补充:`10_serial_loop_chat.py` 10 不是”LangChain 核心概念”示例, 更像一个非常常见的工程小场景: - 怎么在 Python 循环里重复调用模型 - 怎么控制调用节奏 - 怎么记录每轮请求耗时 如果你后面要做轮询、批量处理、限速调用, 这个小示例会比较实用。 ### 补充:`16_history_window_memory.py` 16 是 06 的自然下一步。 当对话越来越长,历史消息越堆越多时, 你需要给历史加一个”窗口”:只保留最近 N 轮。 16 会告诉你: - 为什么不能”永远保留全部历史” - 怎么实现最简单的窗口记忆 - 丢掉旧消息后,模型为什么”记不住”更早的内容 ### 补充:`18_streaming_output.py` 18 会告诉你 `invoke()` 和 `stream()` 的区别: - `invoke()` 等全部写完才返回 - `stream()` 每生成一小块就立刻返回 - 流式输出时,怎么把每个 chunk 拼成完整文本 这是做聊天界面时几乎一定会用到的能力。 ### 补充:`22_document_loader.py` 22 会告诉你: LangChain 怎么加载不同格式的文件(.txt、.csv、.json、整个文件夹)。 所有 Loader 都遵循同一个接口:`loader.load()` 返回 Document 列表。 ### 补充:`23_text_splitters.py` 23 会告诉你: 为什么要把长文档切成小块,以及两种最常用的切分器有什么区别。 - `CharacterTextSplitter`:简单粗暴,按字符数硬切 - `RecursiveCharacterTextSplitter`:更聪明,会尊重段落/句子边界 ### 补充:`24_embeddings_demo.py` 24 把 Embedding 这个”黑盒”打开: - 向量到底长什么样 - 怎么用余弦相似度比较两段文本 - 为什么 Embedding 比关键词搜索更适合做 RAG ### 补充:`25_runnable_passthrough.py` 25 专门帮你理解链里”数据怎么流动”: - `RunnablePassthrough`:输入什么,输出什么(透明管道) - `RunnableParallel`:同一份输入,同时跑多件事 - 经典 RAG 链写法 `{“context”: retriever, “question”: RunnablePassthrough()}` 到底在做什么 ### 补充:`26_few_shot_prompting.py` 26 会告诉你: 怎么在 Prompt 里给模型”例题”,让它更稳定地完成分类任务。 - Zero-shot vs Few-shot 的区别 - LangChain 的 `FewShotChatMessagePromptTemplate` 怎么用 - 动态 Few-shot 怎么根据输入挑最相关的例题 ### 补充:`27_reranking.py` 27 会告诉你: 为什么向量检索的 top-K 不一定最相关,以及怎么用模型做第二轮”精挑”。 - 向量检索是”粗筛” - Reranking 是”精挑” - 重排后的上下文通常能让模型给出更准确的答案 ### 可选补充:`19_fallback_and_retry.py` 19 演示两种让系统更健壮的策略: - `with_retry()`:失败了自动重试 - `with_fallbacks()`:A 不行就换 B ### 可选补充:`20_summarize_map_reduce.py` 20 演示怎么用 Map-Reduce 模式处理长文档: - 把长文档切成小块,各自生成摘要(Map) - 再把所有摘要合并成总摘要(Reduce) ### 可选补充:`21_batch_invoke.py` 21 演示批量处理: - `for + invoke()` 是串行的,一个完了才下一个 - `chain.batch([...])` 是并行的,多个一起发出去 ## 每个示例分别在教什么 ### `00_direct_model_call.py` 目标: 先看懂最基础的模型调用。 你会看到: - `ChatOpenAI(...)` 怎么创建 - `system` 和 `human` 消息是什么 - `model.invoke(...)` 怎么调用 - 模型返回的并不一定是普通字符串 这个示例最重要的价值是: 帮你建立“LangChain 最后还是在调用模型”这个直觉。 ### `09_responses_vs_chat_completions.py` 目标: 分清 OpenAI 底层 API 和 LangChain 上层封装。 你会看到: - `client.responses.create(...)` 怎么调用 - `client.chat.completions.create(...)` 怎么调用 - Responses API 的文本常用 `response.output_text` 读取 - Chat Completions API 的文本常用 `completion.choices[0].message.content` 读取 - 为什么新项目更应该优先理解 Responses API - 为什么很多老教程和兼容服务仍然会出现 Chat Completions API 这个示例最重要的价值是: 帮你把“OpenAI 原生接口”和“LangChain 的 ChatOpenAI 封装”分清楚。 简单说: - OpenAI API 负责提供模型能力 - LangChain 负责把模型能力和 Prompt、Parser、RAG、Tool、Agent 等流程组织起来 如果你想看官方原文, 可以对照这两页: - [Responses API](https://developers.openai.com/api/docs/api-reference/responses) - [Chat Completions API](https://developers.openai.com/api/docs/api-reference/chat) ### `01_prompt_chain.py` 目标: 理解最经典的 Prompt Chain。 你会看到: - `ChatPromptTemplate` - `ChatOpenAI` - `StrOutputParser` - `prompt | model | parser` 这是整个仓库里最核心、最值得反复看的一份示例。 ### `11_chain_step_by_step.py` 目标: 把 `prompt | model | parser` 这条链拆开看。 你会看到: - `prompt.invoke(...)` 本身不会调用模型,它只是先把变量填进模板 - `prompt_value.to_messages()` 能让你看到“最终发给模型的消息长什么样” - `model.invoke(...)` 才是真正发出模型请求的步骤 - `StrOutputParser().invoke(...)` 只是整理结果,不会再次请求模型 如果你看 `01_prompt_chain.py` 时还觉得“链式写法像黑盒”, 那 11 就是专门帮你把黑盒拆开的。 ### `08_server_side_prompt_cache.py` 目标: 理解“服务端 Prompt Caching”和“LangChain 本地缓存”不是一回事。 你会看到: - 为什么要把“稳定内容”放在提示词前面 - 为什么 Prompt 要故意做得足够长 - 为什么第一次通常更像冷启动,后面几次更容易命中 - 怎么从返回结果里读出 `cache_read` 或 `cached_tokens` ### `10_serial_loop_chat.py` 目标: 理解“在循环里重复调用模型”这种工程上很常见的写法。 你会看到: - 怎么把一次模型请求封装成一个函数 - 怎么串行执行多轮调用,而不是并发乱发 - 怎么在两轮之间等待几秒 - 怎么记录每次请求耗时 这个示例不是 LangChain 核心概念课, 但对后面写批处理、轮询、心跳检查这类程序很有帮助。 ### `03_structured_output.py` 目标: 理解“让模型输出固定字段”这件事。 你会看到: - Pydantic 数据结构 - `with_structured_output(...)` - 为什么结构化输出更适合后续程序处理 ### `05_multi_step_workflow.py` 目标: 理解“固定流程的多步骤工作流”。 你会看到: - 第一步:先分析用户问题 - 第二步:用 Python 代码做分支判断 - 第三步:再让模型按策略生成答案 这个示例专门帮你建立一个非常重要的认知: 不是所有复杂流程都需要 Agent。 ### `06_conversation_history.py` 目标: 理解“多轮对话上下文”到底是怎么实现的。 你会看到: - `MessagesPlaceholder` - `chat_history` 为什么是一个消息列表 - 为什么清空历史后,模型会更像“重新开始一段对话” ### `04_simple_rag.py` 目标: 理解一个最小可跑的 RAG 流程。 你会看到: - 读取本地文档 - 切分文档 - 生成 Embedding - 放进向量库 - 检索相关片段 - 基于片段生成答案 ### `07_multi_file_rag.py` 目标: 理解“多个文件一起做知识库”的最小 RAG 版本。 你会看到: - 一个目录下的多份 markdown 文件一起被读取 - 每份文件可以拆成多个 Document - `metadata` 里为什么会放 `source` 和 `section` - 最终答案为什么最好说明参考了哪些来源 ### `12_query_rewrite_rag.py` 目标: 理解“为什么有些 RAG 系统会先改写查询,再去检索”。 你会看到: - Query Rewrite 不等于直接回答用户 - 它更像“先把检索问题整理清楚” - 可以同时比较“原问题检索结果”和“改写后查询检索结果” - 检索结果可以合并、去重,再交给模型组织答案 ### `14_hybrid_search_rag.py` 目标: 理解“为什么很多 RAG 系统会把关键词检索和向量检索结合起来”。 你会看到: - 关键词检索更擅长匹配精确术语 - 向量检索更擅长匹配语义相近表达 - Hybrid Search 会把两边检索结果合并、去重、再统一交给模型 - 最终上下文里可以标注“这个片段是怎么被找出来的” ### `02_agent_with_tools.py` 目标: 理解 Agent 和 Tool 的配合方式。 你会看到: - `@tool` - 给 Agent 暴露本地可调用能力 - Agent 按需决定是否调用工具 - Agent 和普通 Chain 的区别 ### `13_multi_tool_agent.py` 目标: 理解“一个 Agent 可以同时拥有多个工具”。 你会看到: - 不同工具可以分别负责查主笔记、查多文件资料、推荐示例 - Agent 不一定只调用一次工具 - 打印出来的调试轨迹能帮助你看懂 Agent 中间做了什么 ### `15_agent_tool_router.py` 目标: 理解“为什么工具多了以后,常常要先做 Router,再决定给 Agent 哪些工具”。 你会看到: - 先用结构化输出做工具路由分析 - Python 根据路由结果,缩小本次开放给 Agent 的工具范围 - 有些问题不必上 Agent,可以直接走普通回答链 ### `16_history_window_memory.py` 目标: 理解"为什么对话越来越长时,需要给历史消息加窗口"。 你会看到: - 为什么不能永远保留全部历史(token 爆炸) - 怎么用 `trim_to_window()` 只保留最近 N 条消息 - 丢掉旧消息后,模型为什么"记不住"更早的内容 ### `17_reflection_agent_loop.py` 目标: 理解"生成 → 评审 → 改进"的反思循环。 你会看到: - 怎么让"作者"和"评审员"两个角色配合 - 怎么用结构化输出让评审员给出 `is_satisfied` 字段 - 怎么控制循环次数,避免无限反思 ### `18_streaming_output.py` 目标: 理解 `invoke()` 和 `stream()` 的行为区别。 你会看到: - `invoke()` 等全部写完才返回 - `stream()` 每生成一小块就立刻返回 - 流式输出时,怎么把每个 chunk 拼成完整文本 ### `19_fallback_and_retry.py` 目标: 理解两种让系统更健壮的策略。 你会看到: - `with_retry(stop_after_attempt=3)`:失败后自动重试 - `with_fallbacks([备用链])`:主链失败时自动切换备用链 - 两者可以组合使用 ### `20_summarize_map_reduce.py` 目标: 理解怎么用 Map-Reduce 模式处理长文档。 你会看到: - 把长文档切成小块,各自生成摘要(Map) - 把所有摘要合并成总摘要(Reduce) - 为什么不直接一次性把整篇文档塞给模型 ### `21_batch_invoke.py` 目标: 理解批量处理和串行处理的区别。 你会看到: - `for + invoke()` 是串行的,总耗时 ≈ 单个 × 数量 - `chain.batch([...])` 是并行的,总耗时接近单个请求耗时 - 批量处理的数据越多,速度优势越明显 ### `22_document_loader.py` 目标: 理解怎么用不同 Loader 加载不同格式的文件。 你会看到: - `TextLoader`:加载纯文本 - `CSVLoader`:加载 CSV 表格,每行一个 Document - `JSONLoader`:加载 JSON,可指定提取哪个字段 - `DirectoryLoader`:一次加载整个文件夹 ### `23_text_splitters.py` 目标: 理解为什么要把长文档切成小块,以及两种切分器的区别。 你会看到: - `CharacterTextSplitter`:简单粗暴,按字符数硬切 - `RecursiveCharacterTextSplitter`:会尊重段落/句子边界(推荐) - `chunk_size` 和 `chunk_overlap` 怎么影响结果 ### `24_embeddings_demo.py` 目标: 打开 Embedding 这个"黑盒",亲眼看清楚向量相似度。 你会看到: - 向量到底长什么样(一串浮点数) - 怎么用余弦相似度比较两段文本 - 为什么"用词不同但意思相近"的文本,向量也很接近 - 为什么 Embedding 比关键词搜索更适合做 RAG ### `25_runnable_passthrough.py` 目标: 理解数据在链里怎么流动。 你会看到: - `RunnablePassthrough`:输入什么,输出什么(透明管道) - `RunnableParallel`:同一份输入,同时跑多件事,结果合成字典 - `RunnablePassthrough.assign()`:在原字典上追加新字段 - 经典 RAG 链写法到底在做什么 ### `26_few_shot_prompting.py` 目标: 理解怎么在 Prompt 里给模型"例题"来提高输出质量。 你会看到: - Zero-shot(不给例题)vs Few-shot(给例题)的区别 - `FewShotChatMessagePromptTemplate` 怎么用 - 动态 Few-shot 怎么根据输入挑最相关的例题,节省 token ### `27_reranking.py` 目标: 理解为什么向量检索的 top-K 不一定最相关,以及怎么做第二轮"精挑"。 你会看到: - 向量检索是"粗筛"(快,但不一定最准) - 用聊天模型 + 结构化输出给每个候选打相关性分数 - 按新分数重排后,上下文更相关,答案更稳 - 对比"无 reranking"和"有 reranking"的最终答案质量 ## 运行前准备 ### 1. 安装依赖 在项目目录执行: ```bash python -m pip install -r requirements.txt ``` 如果你想明确指定解释器,也可以这样写: ```bash /opt/homebrew/Caskroom/miniforge/base/bin/python -m pip install -r requirements.txt ``` ### 2. 配置环境变量 先复制一份环境变量文件: ```bash cp .env.example .env ``` 然后打开 `.env`,至少填写这些内容: ```env OPENAI_API_KEY=你的真实 API Key OPENAI_MODEL=你要使用的聊天模型 OPENAI_EMBEDDING_MODEL=text-embedding-3-small ``` 如果你使用 OpenAI 官方接口,`OPENAI_BASE_URL` 通常可以留空。 如果你使用兼容 OpenAI 的服务,再补上: ```env OPENAI_BASE_URL=https://your-api-host/v1 ``` ### 3. 哪些示例需要 Embedding 模型 以下示例一定会用到 `OPENAI_EMBEDDING_MODEL`: - `examples/04_simple_rag.py` - `examples/07_multi_file_rag.py` - `examples/12_query_rewrite_rag.py` - `examples/14_hybrid_search_rag.py` - `examples/24_embeddings_demo.py` - `examples/27_reranking.py` 原因是: - `OPENAI_MODEL` 负责聊天/生成 - `OPENAI_EMBEDDING_MODEL` 负责把文本转成向量 它们通常不是同一个模型。 下面这些示例只需要聊天/生成模型, 不需要 Embedding 模型: - `examples/00_direct_model_call.py` - `examples/01_prompt_chain.py` - `examples/02_agent_with_tools.py` - `examples/03_structured_output.py` - `examples/05_multi_step_workflow.py` - `examples/06_conversation_history.py` - `examples/08_server_side_prompt_cache.py` - `examples/09_responses_vs_chat_completions.py` - `examples/10_serial_loop_chat.py` - `examples/11_chain_step_by_step.py` - `examples/13_multi_tool_agent.py` - `examples/15_agent_tool_router.py` - `examples/16_history_window_memory.py` - `examples/17_reflection_agent_loop.py` - `examples/18_streaming_output.py` - `examples/19_fallback_and_retry.py` - `examples/20_summarize_map_reduce.py` - `examples/21_batch_invoke.py` - `examples/22_document_loader.py`(不需要任何模型) - `examples/23_text_splitters.py`(不需要任何模型) - `examples/25_runnable_passthrough.py` - `examples/26_few_shot_prompting.py` ### 4. 关于 08 服务端缓存示例的额外说明 `examples/08_server_side_prompt_cache.py` 最适合用 OpenAI 官方接口来观察。 原因是: - 它依赖服务端 Prompt Caching 能力 - 它希望返回缓存统计字段,例如 `cache_read` 或 `cached_tokens` 如果你使用兼容 OpenAI 的第三方服务: - 代码不一定报错 - 但也可能根本不支持 `prompt_cache_key` - 或者支持调用,却不返回缓存统计信息 ### 5. 关于 09 API 对比示例的额外说明 `examples/09_responses_vs_chat_completions.py` 直接使用 OpenAI Python SDK, 不是 LangChain 封装。 它会连续调用两个接口: - Responses API:`client.responses.create(...)` - Chat Completions API:`client.chat.completions.create(...)` 为什么这个示例放在 LangChain 项目里? 因为初学者很容易把下面几层混在一起: - OpenAI API:底层模型服务接口 - OpenAI Python SDK:Python 里调用 OpenAI API 的官方工具包 - LangChain:在模型调用之上组织 Prompt、Parser、RAG、Tool、Agent 的框架 如果你使用兼容 OpenAI 的第三方服务: - Chat Completions API 更可能被支持 - Responses API 不一定被支持 - `developer` role 不一定被支持 所以如果 09 里 Responses API 报错, 不一定是代码写错, 也可能是当前服务没有实现这个接口。 ## 怎么运行 ### 示例 0:直接调用模型 ```bash python examples/00_direct_model_call.py ``` 这个示例最值得你观察的是: - 输入给模型的是哪些消息 - 返回对象类型是什么 - 为什么后面很多 LangChain 写法,最终都还是围绕 `invoke()` 展开 ### 示例 9:Responses API vs Chat Completions API ```bash python examples/09_responses_vs_chat_completions.py ``` 这个示例会对同一个问题调用两次模型: - 第一次用 Responses API - 第二次用 Chat Completions API 你要重点看: - Responses API 的请求更像 `instructions + input` - Chat Completions API 的请求更像 `messages` 列表 - Responses API 的文本常用 `response.output_text` 读取 - Chat Completions API 的文本常用 `completion.choices[0].message.content` 读取 这个示例的重点不是让你马上放弃 Chat Completions, 而是帮你看清楚: LangChain 的 `ChatOpenAI` 是上层封装, Responses API 和 Chat Completions API 是更底层的 OpenAI 接口形态。 ### 示例 1:最基础的 Prompt Chain ```bash python examples/01_prompt_chain.py ``` 这个示例最值得你观察的是这行: ```python chain = prompt | model | parser ``` 你可以把它理解成一条流水线: 1. 先把输入填进 Prompt 模板 2. 再调用模型 3. 最后把输出整理成普通字符串 ### 示例 11:把一条 Chain 拆开看 ```bash python examples/11_chain_step_by_step.py ``` 这个示例最值得你观察的是: - `prompt.invoke(...)` 只是填模板,不会调用模型 - `prompt_value.to_messages()` 能看到真正要发给模型的消息 - `model.invoke(...)` 才是真正发送请求 - `parser.invoke(...)` 只是整理结果,不会再次请求模型 如果你看完示例 1 以后, 还是觉得 `prompt | model | parser` 有一点抽象, 那这个示例会非常适合你。 ### 示例 8:服务端 Prompt Cache ```bash python examples/08_server_side_prompt_cache.py ``` 这个示例会连续发起 3 次请求。 你要重点看: - 第 1 次请求通常更像冷启动 - 第 2 次和第 3 次如果前缀稳定,更容易看到 `cache_read` 或 `cached_tokens` 大于 0 - 这不是 LangChain 把最终答案缓存到了本地,而是模型服务端复用了长前缀 这个示例最容易帮你建立一个重要认知: “模型调用的优化”不只有 Prompt 写法, 还有请求结构设计,比如让稳定前缀尽量稳定、尽量靠前。 ### 示例 3:结构化输出 ```bash python examples/03_structured_output.py ``` 这个示例最值得你观察的是: - `LearningRequest` - `model.with_structured_output(...)` 它会把一段自然语言学习需求,整理成固定字段。 ### 示例 5:多步骤 Workflow ```bash python examples/05_multi_step_workflow.py ``` 这个示例最值得你观察的是: - 第一步分析结果会先被打印出来 - 第二步会明确告诉你 Python 选择了哪种讲解策略 - 第三步才是最终回答 也就是说,这个示例不是“黑盒一下子出答案”, 而是把中间过程摊开给你看。 ### 示例 6:多轮对话历史 ```bash python examples/06_conversation_history.py ``` 这个示例最值得你观察的是: - 第二轮追问为什么能接住第一轮上下文 - `/history` 打印出来的历史消息到底长什么样 - `/clear` 之后,为什么模型会更像“重新开始” ### 示例 4:最小 RAG ```bash python examples/04_simple_rag.py ``` 这个示例会读取本地文件: ```text data/langchain_notes.md ``` 然后先检索相关片段,再回答问题。 你运行时要重点看终端里先打印出来的“检索到的上下文片段”。 那部分内容能帮助你真正看懂: RAG 回答前,到底先找到了哪些资料。 ### 示例 7:多文件 RAG ```bash python examples/07_multi_file_rag.py ``` 这个示例会读取目录: ```text data/rag_demo_docs/ ``` 它最值得你观察的是: - 知识库里到底加载了哪些来源文件 - 检索结果是从哪些不同文件里找出来的 - 为什么 `source`、`section` 这类 metadata 对调试很重要 ### 示例 12:查询改写版 RAG ```bash python examples/12_query_rewrite_rag.py ``` 这个示例会读取目录: ```text data/rag_demo_docs/ ``` 它最值得你观察的是: - 模型先产出的不是最终答案,而是“更适合检索的查询” - 原问题检索和改写后检索,命中的片段可能不完全一样 - 最终上下文是怎么把两边结果合并去重的 - Query Rewrite 为什么更像检索前处理,而不是直接回答 ### 示例 14:混合检索版 RAG ```bash python examples/14_hybrid_search_rag.py ``` 这个示例会读取目录: ```text data/rag_demo_docs/ ``` 它最值得你观察的是: - 关键词检索和向量检索各自命中了什么 - 混合检索为什么不是“选一个”,而是“两边都要” - 合并后的上下文片段会标注自己是怎么被找出来的 - 为什么精确术语和语义相近表达,适合用不同方式一起找 ### 示例 2:Agent + Tool ```bash python examples/02_agent_with_tools.py ``` 这个示例也会读取: ```text data/langchain_notes.md ``` 但它和 RAG 的区别是: - RAG:我们先设计好“先检索,再回答” - Agent:让模型自己决定“要不要调用工具、调用哪个工具” ### 示例 13:多工具 Agent ```bash python examples/13_multi_tool_agent.py ``` 这个示例最值得你观察的是: - Agent 这次不只拥有一个工具 - 不同工具会负责不同事情:查笔记、查多文件资料、推荐示例 - 调试轨迹里能看到 Agent 有没有连续调用多个工具 - 多工具 Agent 比最小 Agent 更接近真实应用 ### 示例 15:Agent Tool Router ```bash python examples/15_agent_tool_router.py ``` 这个示例最值得你观察的是: - 系统会先判断这次问题应该走哪条工具路由 - 不是每次都把所有工具全部暴露给 Agent - 如果路由判断“不需要工具”,就会直接走普通回答链 - Tool Router 能帮助 Agent 更稳、更省,也更容易调试 ### 示例 10:串行循环调用模型(可选) ```bash python examples/10_serial_loop_chat.py ``` 这个示例更偏工程练习, 不是 LangChain 核心概念课。 它最值得你观察的是: - 怎么把一次模型请求封装成一个函数 - 怎么在 `for` 循环里重复调用模型 - 怎么在两轮之间等待几秒 - 怎么打印每次请求耗时 如果你后面要写批处理、限速调用、轮询任务, 这个小示例会比较实用。 ### 示例 16:窗口记忆 ```bash python examples/16_history_window_memory.py ``` 这个示例最值得你观察的是: - `/window` 命令打印的"保留/丢弃"状态 - 连续问 4~5 个问题后,回头问"我最开始问的是什么?" - 模型已经"忘了"窗口之外的内容 ### 示例 17:反思循环 Agent ```bash python examples/17_reflection_agent_loop.py ``` 这个示例最值得你观察的是: - "作者"和"评审员"两个角色怎么配合 - 每一轮的评审打分和反馈 - 循环什么时候停止(评审满意 or 达到最大轮数) ### 示例 18:流式输出 ```bash python examples/18_streaming_output.py ``` 这个示例最值得你观察的是: - `invoke()` 要等多久才开始显示结果 - `stream()` 第一个字多快就出现了 - 两种方式的总耗时和最终文本其实差不多 ### 示例 19:备用方案与重试 ```bash python examples/19_fallback_and_retry.py ``` 这个示例最值得你观察的是: - 演示 2 会故意用错误的模型名让主链失败 - 备用链是怎么自动接管的 - retry 和 fallback 组合使用的效果 ### 示例 20:长文档摘要(Map-Reduce) ```bash python examples/20_summarize_map_reduce.py ``` 这个示例最值得你观察的是: - 文档被切成了几个小块 - 每个小块的局部摘要长什么样 - 最终总摘要是怎么把所有局部摘要合并的 ### 示例 21:批量调用 ```bash python examples/21_batch_invoke.py ``` 这个示例最值得你观察的是: - 串行和批量的耗时对比 - `batch()` 的输入格式是一个列表,每项是一个参数字典 - 批量处理的数据越多,速度优势越明显 ### 示例 22:文档加载器 ```bash python examples/22_document_loader.py ``` 这个示例不需要调用模型,不需要 API Key。 它最值得你观察的是: - 每种 Loader 返回的 Document 列表结构 - `page_content` 和 `metadata` 里各有什么 - CSVLoader 的每一行变成了一个 Document ### 示例 23:文本切分器 ```bash python examples/23_text_splitters.py ``` 这个示例不需要调用模型,不需要 API Key。 它最值得你观察的是: - 同一段文本,两种切分器切出来的结果完全不同 - `chunk_size` 和 `chunk_overlap` 怎么影响块数和每块长度 - 为什么 `RecursiveCharacterTextSplitter` 是推荐选择 ### 示例 24:Embeddings 与向量相似度 ```bash python examples/24_embeddings_demo.py ``` 这个示例需要 Embedding 模型。 它最值得你观察的是: - 向量到底长什么样(一串浮点数) - 三组测试句子的余弦相似度对比 - 为什么"用词不同但意思相近"的句子,相似度也很高 ### 示例 25:RunnablePassthrough ```bash python examples/25_runnable_passthrough.py ``` 这个示例最值得你观察的是: - Demo 1~3 不需要调用模型,只讲 Runnable 本身 - Demo 4 用 RunnableParallel 搭了一条简化版 RAG 链 - Demo 5 把 Demo 4 拆开,展示了等价的普通 Python 代码 ### 示例 26:Few-Shot 提示 ```bash python examples/26_few_shot_prompting.py ``` 这个示例最值得你观察的是: - Zero-shot 和 Few-shot 的输出格式对比 - `FewShotChatMessagePromptTemplate` 怎么管理例题 - 动态 Few-shot 的 Prompt 比全部例题的 Prompt 短多少 ### 示例 27:检索结果重排序 ```bash python examples/27_reranking.py ``` 这个示例需要 Embedding 模型和聊天模型。 它最值得你观察的是: - 初始向量检索的顺序 vs 模型重排后的顺序 - 模型给每个候选打的分数和理由 - "无 reranking"和"有 reranking"的最终答案对比 ## 建议你怎么读代码 ### 先读 `00_direct_model_call.py` 建议按这个顺序看: 1. `main()` 2. `build_messages()` 3. `create_model()` 4. `message_to_text()` 重点理解: - `system` 和 `human` 消息分别是什么 - `model.invoke(...)` 到底吃什么输入 - 为什么模型返回的不是普通字符串 ### 再读 `09_responses_vs_chat_completions.py` 建议按这个顺序看: 1. `main()` 2. `call_responses_api()` 3. `call_chat_completions_api()` 4. `extract_responses_text()` 5. `extract_chat_completions_text()` 重点理解: - OpenAI Python SDK 的 `OpenAI(...)` 不是 LangChain 的 `ChatOpenAI(...)` - Responses API 可以用 `instructions` 和 `input` 表达一次请求 - Chat Completions API 主要围绕 `messages` 列表组织一次请求 - 两个 API 最终都在调用模型,但返回对象结构不一样 - LangChain 更关心的是把模型调用放进一条可组合的应用流程里 ### 再读 `01_prompt_chain.py` 建议按这个顺序看: 1. `main()` 2. `build_chain()` 3. `create_model()` 4. `check_environment()` 重点理解: - `load_dotenv()`:把 `.env` 里的配置加载进来 - `ChatPromptTemplate`:提示词模板 - `ChatOpenAI`:LangChain 封装后的模型对象 - `StrOutputParser`:把结果转成普通文本 - `invoke()`:真正执行这条链 ### 再读 `11_chain_step_by_step.py` 建议按这个顺序看: 1. `main()` 2. `run_step_by_step()` 3. `print_formatted_messages()` 4. `show_equivalent_chain_code()` 重点理解: - `prompt.invoke(...)` 和 `model.invoke(...)` 不是一回事 - Prompt 模板先产出消息,再由模型真正消费这些消息 - Parser 处理的是模型结果,不是用户原始输入 - 一行 `prompt | model | parser` 背后其实就是几步普通 Python 调用 ### 再补读 `08_server_side_prompt_cache.py` 建议重点看: 1. `load_stable_reference_text()` 2. `build_messages()` 3. `model.invoke(..., prompt_cache_key=...)` 4. `extract_cache_read_tokens()` 重点理解: - 服务端缓存更关心“长而稳定的前缀” - 动态问题为什么最好放到最后 - 判断有没有命中缓存,最好看返回里的 usage 字段,不要靠感觉猜 ### 再读 `03_structured_output.py` 建议重点看: 1. `LearningRequest` 2. `build_extraction_chain()` 3. `model.with_structured_output(...)` 重点理解: - 为什么先定义字段结构 - 为什么结构化结果更适合后续代码处理 - 为什么这时不一定需要 `StrOutputParser` ### 再读 `05_multi_step_workflow.py` 建议重点看: 1. `QuestionAnalysis` 2. `build_analysis_chain()` 3. `choose_strategy()` 4. `build_answer_chain()` 重点理解: - “多步骤”不等于“必须 Agent” - 模型可以先分析,再生成 - Python 完全可以负责流程控制 - 固定 Workflow 比 Agent 更容易调试 ### 再读 `06_conversation_history.py` 建议重点看: 1. `build_chat_chain()` 2. `MessagesPlaceholder(variable_name="chat_history")` 3. `history.append(...)` 4. `/history` 和 `/clear` 这两个小命令 重点理解: - 历史消息不是自动来的,而是程序自己保存的 - 下一轮之所以能接住上下文,是因为历史被重新传了进去 - 清空历史后,模型就看不到之前那段上下文了 ### 再读 `04_simple_rag.py` 建议重点看: 1. `load_note_documents()` 2. `build_vector_store()` 3. `retriever.invoke(...)` 4. `build_answer_chain()` 重点理解: - 文档为什么要先切块 - Embedding 为什么不是聊天模型 - Retriever 只负责找资料,不负责回答 - RAG 的回答本质上仍然是 Prompt + Model ### 再读 `07_multi_file_rag.py` 建议重点看: 1. `split_markdown_file()` 2. `load_demo_documents()` 3. `doc.metadata` 4. `print_available_sources()` 重点理解: - RAG 的资料源不一定只有一个文件 - 一个文件也可以继续拆成多个可检索片段 - metadata 能帮助你解释“这段内容来自哪里” ### 再读 `12_query_rewrite_rag.py` 建议重点看: 1. `QueryRewritePlan` 2. `build_query_rewrite_chain()` 3. “原问题检索” 和 “改写后查询检索” 这两段打印 4. `merge_unique_documents()` 重点理解: - Query Rewrite 的目标是帮助检索,不是直接替用户回答 - 改写后的 query 往往会补出更明确的术语 - 多轮检索结果可以合并,再统一交给模型回答 ### 再读 `14_hybrid_search_rag.py` 建议重点看: 1. `extract_query_terms()` 2. `keyword_search()` 3. `merge_hybrid_results()` 4. `format_hybrid_context()` 重点理解: - 关键词检索和向量检索不是互斥关系 - Hybrid Search 的核心不是“更花哨”,而是“尽量别漏掉有用片段” - 混合检索结果最好说明“它是怎么被命中的” ### 再读 `02_agent_with_tools.py` 建议重点看: 1. `@tool` 2. `list_note_topics()` 3. `search_learning_notes()` 4. `build_agent()` 5. `agent.invoke(...)` 重点理解: - Tool 本质上就是“暴露给模型调用的函数” - Agent 会先判断,再决定要不要用 Tool - Agent 的流程比固定 Chain 更灵活,但也更难理解 ### 最后读 `13_multi_tool_agent.py` 建议重点看: 1. `EXAMPLE_CATALOG` 2. `search_learning_notes()` 3. `search_demo_docs()` 4. `suggest_examples_by_topic()` 5. `print_agent_trace()` 重点理解: - 一个 Agent 可以同时拿到多种工具 - Tool 不一定只能查外部系统,也可以是本地规则函数 - 看调试轨迹是理解 Agent 行为的好习惯 ### 再最后读 `15_agent_tool_router.py` 建议重点看: 1. `ToolRoutePlan` 2. `build_route_chain()` 3. `choose_tools_by_route()` 4. `build_routed_agent()` 5. `build_direct_answer_chain()` 重点理解: - 路由和回答是两步不同工作 - 不一定每次都要把全部工具直接交给 Agent - 如果问题足够简单,直接回答通常更省 ### 再读 `16_history_window_memory.py` 建议重点看: 1. `trim_to_window()` 2. `print_window_status()` 3. `/window` 命令打印的状态 重点理解: - 为什么要给历史加窗口(token 爆炸) - 为什么保留"最后"几条,而不是"最前"几条 - 窗口和 `/clear` 的区别 ### 再读 `17_reflection_agent_loop.py` 建议重点看: 1. `ReviewResult` 2. `build_writer_chain()` 3. `build_reviewer_chain()` 4. `run_reflection_loop()` 重点理解: - "作者"和"评审员"是两个不同的 Chain - 评审员用结构化输出返回 `is_satisfied` - 循环停止条件是"满意"或"达到最大轮数" ### 再读 `18_streaming_output.py` 建议重点看: 1. `demo_invoke()` 2. `demo_stream()` 3. `for chunk in chain.stream(...)` 这个遍历方式 重点理解: - `invoke()` 返回一个完整字符串 - `stream()` 返回一个生成器,每次给一小块 - 两种方式的最终文本内容是一样的 ### 再读 `19_fallback_and_retry.py` 建议重点看: 1. `chain.with_retry(stop_after_attempt=3)` 2. `primary_chain.with_fallbacks([fallback_chain])` 3. retry + fallback 组合使用 重点理解: - retry 适合处理偶发网络问题 - fallback 适合处理模型本身不可用 - 两者可以叠加使用 ### 再读 `20_summarize_map_reduce.py` 建议重点看: 1. `split_into_chunks()` 2. `build_map_chain()` 3. `build_reduce_chain()` 4. 两步的调用顺序 重点理解: - Map 阶段:每块各自生成摘要 - Reduce 阶段:把所有摘要合并成总摘要 - 总调用次数 = 块数 + 1 ### 再读 `21_batch_invoke.py` 建议重点看: 1. `demo_serial()` 里的 for 循环 2. `demo_batch()` 里的 `chain.batch(inputs)` 3. 两种方式的耗时对比 重点理解: - `batch()` 的输入是一个列表,每项是一个参数字典 - `batch()` 的返回也是列表,顺序和输入一一对应 - 并行处理的数据越多,速度优势越明显 ### 再读 `22_document_loader.py` 建议重点看: 1. `demo_text_loader()` 2. `demo_csv_loader()` 3. `demo_json_loader()` 4. `demo_directory_loader()` 重点理解: - 所有 Loader 返回的都是 Document 列表 - 每个 Document 有 `page_content` 和 `metadata` - 不同 Loader 的区别在于"怎么把文件变成 Document" ### 再读 `23_text_splitters.py` 建议重点看: 1. `demo_character_splitter()` 2. `demo_recursive_splitter()` 3. `demo_different_chunk_sizes()` 4. `demo_side_by_side_comparison()` 重点理解: - `chunk_size` 控制每块最大多少字符 - `chunk_overlap` 控制相邻两块重叠多少字符 - `RecursiveCharacterTextSplitter` 会尽量在自然边界切分 ### 再读 `24_embeddings_demo.py` 建议重点看: 1. `cosine_similarity()` 2. `embeddings.embed_documents()` 3. `prepare_sentence_pairs()` 里的三组测试句子 重点理解: - Embedding 把文本变成一个浮点数列表(向量) - 余弦相似度越接近 1,语义越相似 - 即使"用词不同但意思相近",向量也很接近 ### 再读 `25_runnable_passthrough.py` 建议重点看: 1. `demo_passthrough_basic()` 2. `demo_parallel()` 3. `demo_passthrough_assign()` 4. `demo_rag_like_chain()` 5. `demo_equivalent_plain_python()` 重点理解: - `RunnablePassthrough` 是"透明管道" - `RunnableParallel` 同时跑多件事,结果合成字典 - 经典 RAG 链写法本质上是 RunnableParallel + Prompt + Model ### 再读 `26_few_shot_prompting.py` 建议重点看: 1. `EXAMPLES` 列表 2. `demo_zero_shot()` 3. `demo_manual_few_shot()` 4. `demo_few_shot_template()` 5. `KeywordExampleSelector` 6. `demo_dynamic_few_shot()` 重点理解: - 给模型"例题"能让输出格式更稳定 - `FewShotChatMessagePromptTemplate` 比手写字符串更整洁 - 动态 Few-shot 能节省 token ### 再读 `27_reranking.py` 建议重点看: 1. `initial_retrieve()` 2. `RelevanceScore` 3. `rerank_with_model()` 4. `sort_by_rerank_score()` 5. `compare_with_and_without_reranking()` 重点理解: - 向量检索是"粗筛",重排序是"精挑" - 用结构化输出稳定拿到相关性分数 - 重排后的上下文通常能让模型给出更准确的答案 ## `data/` 目录里的资料分别是干什么的 ### `data/langchain_notes.md` 这是项目里的主学习笔记。 它有几个作用: 1. 给 `examples/04_simple_rag.py` 提供可检索的本地资料 2. 给 `examples/02_agent_with_tools.py` 提供可查询的本地资料 3. 给 `examples/13_multi_tool_agent.py` 提供可查询的本地资料 4. 给 `examples/15_agent_tool_router.py` 提供可查询的本地资料 5. 给 `examples/08_server_side_prompt_cache.py` 提供“稳定且足够长”的参考前缀 ### `data/rag_demo_docs/` 这是专门给 `examples/07_multi_file_rag.py`、`examples/12_query_rewrite_rag.py`、`examples/13_multi_tool_agent.py`、`examples/14_hybrid_search_rag.py`、`examples/15_agent_tool_router.py` 准备的多文件演示资料目录。 它的作用是帮助你看懂: - 多个文件怎么一起读进知识库 - 文档 metadata 怎么保留来源信息 - 检索结果为什么最好带文件名和小节名 你后面完全可以自己往这个目录里继续加新文件, 然后重新运行 07、12 或 14,观察检索结果怎么变化。 ## 初学者最容易混淆的几组概念 ### OpenAI API 和 LangChain - OpenAI API:模型服务本身提供的底层接口 - LangChain:把模型调用和 Prompt、Parser、RAG、Tool、Agent 串起来的上层框架 ### Responses API 和 Chat Completions API - Responses API:OpenAI 新一代统一模型接口,更适合新项目优先理解 - Chat Completions API:传统聊天接口,很多老教程、旧项目和兼容服务仍然常见 ### Prompt 和 Model - Prompt:你怎么提要求 - Model:真正负责生成内容的模型 ### Model 和 Parser - Model:先生成原始结果 - Parser:再把结果整理成更适合使用的形式 ### Chain 和 Workflow - Chain:一条比较直接的流水线 - Workflow:由多个步骤组成的固定流程 ### 单轮问答和多轮对话 - 单轮问答:只看当前这一次输入 - 多轮对话:当前输入还会带上前面的历史消息 ### Workflow 和 Agent - Workflow:流程由你提前设计好 - Agent:模型会自己判断下一步动作 ### Retriever 和 Model - Retriever:负责找资料 - Model:负责组织语言、生成答案 ### 内容正文和 Metadata - 正文:真正拿来做语义检索和回答的内容 - Metadata:来源文件、标题、小节等辅助信息 ### RAG 和 Agent - RAG:先查资料再回答 - Agent:先判断再行动 ### invoke() 和 stream() - `invoke()`:等模型全部写完,一次性返回完整结果 - `stream()`:模型每生成一小块就立刻返回,需要自己拼接 ### Retry 和 Fallback - Retry:失败了再试同一个(适合偶发网络问题) - Fallback:A 不行就换 B(适合模型本身不可用) ### Map-Reduce 和 Refine - Map-Reduce:每块各自摘要,再合并成总摘要(并行) - Refine:第一块摘要 → 和第二块合并 → 再和第三块合并(串行) ### 聊天模型和 Embedding 模型 - 聊天模型(ChatOpenAI):输入文本,输出文本 - Embedding 模型(OpenAIEmbeddings):输入文本,输出一串数字(向量) ### CharacterTextSplitter 和 RecursiveCharacterTextSplitter - CharacterTextSplitter:简单粗暴,按字符数硬切 - RecursiveCharacterTextSplitter:会按优先级尝试多种分隔符,尽量在自然边界切 ### 关键词检索和向量检索 - 关键词检索:按字面匹配,找"有没有同一个词" - 向量检索:按语义匹配,找"意思接不接近" ### RunnablePassthrough 和 RunnableParallel - RunnablePassthrough:输入什么就输出什么(透明管道) - RunnableParallel:同一份输入同时跑多件事,结果合成字典 ### Zero-shot 和 Few-shot - Zero-shot:只告诉模型任务是什么,不给例题 - Few-shot:在 Prompt 里给模型几个例题,让它模仿格式 ### 初始检索和 Reranking - 初始检索:用向量相似度"粗筛",快但不一定最准 - Reranking:用模型重新打分"精挑",更准但更慢 ## 常见报错与原因 ### 1. `OPENAI_API_KEY` 没设置 现象: 程序一启动就报错。 原因: 你还没配置 `.env`,或者 `.env` 里没有 `OPENAI_API_KEY`。 ### 2. 模型名报错 现象: 提示模型不存在、没有权限,或者接口不支持。 原因: 你填写的 `OPENAI_MODEL` 在你当前账号或服务里不可用。 解决: 把 `.env` 里的模型名改成你自己实际能用的那个。 ### 3. RAG 示例报 embedding 相关错误 现象: `examples/04_simple_rag.py`、`examples/07_multi_file_rag.py`、`examples/12_query_rewrite_rag.py` 或 `examples/14_hybrid_search_rag.py` 运行失败, 但 `examples/00_direct_model_call.py` 或 `examples/01_prompt_chain.py` 可以运行。 原因: 你的服务支持聊天模型,但不支持 embedding,或者 `OPENAI_EMBEDDING_MODEL` 填错了。 解决: 先确认你的服务是否支持 embeddings,再检查 embedding 模型名称。 ### 4. `OPENAI_BASE_URL` 相关问题 现象: 连不上接口,或者接口路径不对。 原因: 你正在使用兼容 OpenAI 的服务,但 `OPENAI_BASE_URL` 没填对。 解决: 确认服务提供方给你的完整 Base URL。 ### 5. 09 示例里 Responses API 报错 现象: `examples/09_responses_vs_chat_completions.py` 里 Chat Completions 能跑, 但 Responses API 报错。 原因: 你当前使用的可能是兼容 OpenAI 的第三方服务, 它实现了 `/chat/completions`, 但没有实现 `/responses`。 解决: 优先用 OpenAI 官方接口运行 09。 如果必须使用第三方兼容服务, 就要以该服务自己的文档为准。 ### 6. 09 示例里 `developer` role 报错 现象: Chat Completions API 报 role 相关错误。 原因: OpenAI 当前文档里会使用 `developer` 表示应用开发者指令, 但有些旧模型或兼容服务只认识旧式 `system` role。 解决: 如果你确认服务不支持 `developer`, 可以把 09 里的 `role: "developer"` 改成 `role: "system"` 再试。 ### 7. Agent 示例报 tool calling 相关错误 现象: `examples/02_agent_with_tools.py`、`examples/13_multi_tool_agent.py` 或 `examples/15_agent_tool_router.py` 报错,但普通 Chain 可以运行。 原因: 你当前模型或服务可能不支持 tool/function calling。 解决: 先确认所用模型支持工具调用,再运行 Agent 示例。 ### 8. Structured Output 示例报错 现象: `examples/03_structured_output.py`、`examples/05_multi_step_workflow.py`、`examples/12_query_rewrite_rag.py` 或 `examples/15_agent_tool_router.py` 报错。 原因: 你当前模型或兼容服务不支持稳定的结构化输出,或者 function calling 支持不完整。 解决: 先运行 `examples/00_direct_model_call.py` 和 `examples/01_prompt_chain.py` 验证基础聊天链路,再单独排查结构化输出能力。 ### 9. 多轮对话示例看起来“没有记忆” 现象: `examples/06_conversation_history.py` 里第二轮回答没接住第一轮上下文。 原因: 可能是你上一轮的问题本身不够明确,或者你中途用了 `/clear` 清空了历史。 解决: 先问一个明确的第一轮问题,再追问“那它和 Agent 有什么区别?”这种依赖上下文的问题,同时确认没有清空 history。 ### 10. 服务端缓存示例一直看不到命中 现象: `examples/08_server_side_prompt_cache.py` 可以运行, 但 `cache_read` / `cached_tokens` 一直是 0。 原因: - 你当前使用的服务不支持 Prompt Caching - 你的模型版本不支持这项能力 - 提示词前缀还不够长 - 每次请求前缀变动太大,没形成稳定前缀 解决: 1. 优先用 OpenAI 官方接口观察这个示例 2. 尽量不要改动示例里“固定参考资料”前面的结构 3. 先连续运行几次,再看后续请求是否出现缓存命中 ## 最推荐的练习方式 最推荐的练法不是“只运行一次”, 而是边跑边改。 你可以按这个顺序自己动手: 1. 先运行 `examples/00_direct_model_call.py` 2. 改一下 `system` 消息内容,再观察回答风格 3. 再运行 `examples/09_responses_vs_chat_completions.py` 4. 对比 `response.output_text` 和 `completion.choices[0].message.content` 5. 再运行 `examples/01_prompt_chain.py` 6. 改一下 Prompt 模板,再观察结果变化 7. 再运行 `examples/11_chain_step_by_step.py` 8. 对照终端输出,看清楚 `prompt.invoke(...)`、`model.invoke(...)`、`parser.invoke(...)` 各自做什么 9. 如果你对性能优化感兴趣,这时可以插跑 `examples/08_server_side_prompt_cache.py` 10. 观察为什么稳定前缀更容易命中服务端缓存 11. 再运行 `examples/03_structured_output.py` 12. 改一下 Pydantic 字段,再观察结构化结果变化 13. 再运行 `examples/05_multi_step_workflow.py` 14. 改一下 `choose_strategy()` 里的分支逻辑 15. 再运行 `examples/06_conversation_history.py` 16. 观察 `/history` 和 `/clear` 对回答的影响 17. 再运行 `examples/04_simple_rag.py` 18. 自己往 `data/langchain_notes.md` 加一节新内容,再重新提问 19. 再运行 `examples/07_multi_file_rag.py` 20. 试着修改 `data/rag_demo_docs/` 里的资料内容,再观察检索变化 21. 再运行 `examples/12_query_rewrite_rag.py` 22. 对比“原问题检索”和“改写后检索”的差异 23. 再运行 `examples/14_hybrid_search_rag.py` 24. 对比“关键词检索命中”和“向量检索命中”的差异 25. 再运行 `examples/02_agent_with_tools.py` 26. 再运行 `examples/13_multi_tool_agent.py` 27. 最后再运行 `examples/15_agent_tool_router.py` 28. 如果你想补一个工程向练习,再运行 `examples/10_serial_loop_chat.py` 这样学,通常会比“只看不改”快很多。 ## 你下一步可以继续做什么 当你把这个仓库看懂后,下一步我建议你继续练这几个方向: 1. 让结构化输出返回更复杂的嵌套字段 2. 给 Workflow 增加更多固定步骤 3. 给多轮对话加上更真实的历史裁剪策略(如 Summary + Window) 4. 给 RAG 加上更真实的文档切块策略(如按语义切块) 5. 让 Agent 同时拥有多个工具 6. 把分类、提取、检索、总结串成一个更完整的小应用 7. 用 `RunnablePassthrough` 和 `RunnableParallel` 搭建更复杂的链 8. 给 Few-shot 加上 `SemanticSimilarityExampleSelector`,做真正的语义动态选择 9. 用专门的 cross-encoder reranker(如 Cohere Rerank)替换示例 27 里的聊天模型打分 10. 把 Map-Reduce 和 RAG 结合起来,做一个”长文档问答系统” 如果你愿意,我下一步还可以继续直接在这个项目里往下补: - `28_langgraph_intro.py`:LangGraph 入门,用状态图代替 Python 循环控制 Agent - `29_self_query_retriever.py`:Self-Query Retriever,让模型自己生成检索过滤条件 - `30_multi_modal_rag.py`:多模态 RAG,处理图片 + 文本混合输入 - 一个”每个示例应该重点观察什么”的配套笔记版说明