LangChain

LangChain

目录


一、LangChain 是什么(架构总览)

一句话定位

LangChain = 调模型之上的"编排层"。 它不替你调模型(那还是 OpenAI/DeepSeek 的活),它解决的是"调完模型之后那一堆 plumbing":拼提示词、解析输出、接记忆、接检索、接工具、把流程串成管道。

你之前 W1-W3 手写过的 RealLLM / prompt_lib / fc_loop / MemoryManager / Chroma,在 LangChain 里都有标准件对应。学 LangChain 不是学新东西,是把你的"手动接线"换成"标准管道"。

全景架构图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
┌──────────────────────────────────────────────────────────────┐
│                      你的应用 / Agent                          │
├──────────────────────────────────────────────────────────────┤
│  LangChain 编排层(本文重点)                                  │
│                                                                │
│   Prompt ──▶ Model ──▶ OutputParser        (基础链 LCEL)     │
│      │         │            │                                    │
│      ▼         ▼            ▼                                    │
│   Memory   Retriever      Tool ◀── Agent(决定调谁)            │
│   (记忆)    (RAG检索)     (工具)                                 │
│                                                                │
│   RunnableParallel / Passthrough / Lambda / Branch(组合件)    │
│   LangGraph(状态图:把上面这些编排成有环的流程)               │
├──────────────────────────────────────────────────────────────┤
│  langchain-core / langchain-openai / community(接入层)        │
├──────────────────────────────────────────────────────────────┤
│  模型供应商 API(DeepSeek / OpenAI / 通义 / Kimi ...)          │
└──────────────────────────────────────────────────────────────┘

包结构 —— 别一股脑 pip install langchain 就完事

1
2
3
4
5
6
pip install langchain-core          # 核心抽象:Runnable / Prompt / Parser / 基础接口(必装)
pip install langchain-openai        # OpenAI 及兼容协议(DeepSeek 走这)的模型接入
pip install langchain-community     # 社区集成:各种向量库、文档加载器、第三方工具
pip install langchain               # 高层封装:agent / chain / 一些 conveniences(可选)
pip install langgraph              # 状态图编排(W5 用,独立包)
pip install python-dotenv          # 读 .env 里的密钥
干啥你现在用得到吗
langchain-coreRunnable 协议、Prompt、Parser、Message 类型✅ 必用
langchain-openaiChatOpenAI 模型封装✅ 必用
langchain-communityChroma / FAISS / 各类 loader✅ 做 RAG 用
langchain旧的 LLMChainAgent 封装⚠️ 历史包袱多,新项目优先用 core + LCEL
langgraph状态图 / 多轮循环编排✅ W5 进阶

⚠️ 版本兼容是头号坑:LangChain 迭代极快,旧博客里的 LLMChain(...)ConversationChain 在新版已弃用。本文全部基于 LCEL(core 0.2+) 写法,这是目前最稳、官方主推的范式。

裸调 vs LangChain 对照(先理解痛点)

1
2
3
4
5
6
7
# ===== 裸调(你 W1 的写法)=====
import openai
resp = openai.ChatCompletion.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "你好"}]
)
text = resp["choices"][0]["message"]["content"]   # 每次都要抠这一长串
1
2
3
4
5
6
7
# ===== LangChain(声明式)=====
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

chain = ChatPromptTemplate.from_messages([("user", "{q}")]) | ChatOpenAI(...) | StrOutputParser()
text = chain.invoke({"q": "你好"})   # 一行拿到字符串,不用抠 dict

差别:裸调你写 plumbing,LangChain 你只声明"要什么流程"


二、安装与环境

pip install —— 装依赖

1
pip install -U langchain langchain-openai langchain-core langchain-community python-dotenv

-U 升级到最新版;建议锁版本做笔记:pip freeze > requirements.txt

.env + load_dotenv —— 密钥分离

1
2
3
4
5
import os
from dotenv import load_dotenv

load_dotenv()                          # 自动读取项目根目录的 .env 文件
api_key = os.getenv("DEEPSEEK_API_KEY")  # 从环境变量取,绝不硬编码进代码

.env 文件内容(放项目根目录):

1
DEEPSEEK_API_KEY=sk-xxxxxxx

⚠️ 密钥红线.env 必须写进 .gitignore,绝对不能 commit 进仓库。一旦泄露,第一时间去平台注销密钥并换新的(你一直强调的安全意识,这里再次强调)。


三、Model 模型:ChatModel

ChatOpenAI —— 统一接口调任意兼容模型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from langchain_openai import ChatOpenAI
import os

# DeepSeek 走 OpenAI 兼容协议:换 base_url 就行
model = ChatOpenAI(
    model="deepseek-chat",                       # 模型名(DeepSeek 用 deepseek-chat / deepseek-reasoner)
    api_key=os.getenv("DEEPSEEK_API_KEY"),       # 从环境变量取
    base_url="https://api.deepseek.com",         # 兼容协议入口
    temperature=0.7,                             # 随机性:0= deterministic,1= 发散
    max_tokens=1024,                             # 单次最多生成多少 token
)
参数作用常用值
model模型名deepseek-chat / gpt-4o / qwen-max
temperature采样温度0(严谨)/ 0.7(创意)/ 1(发散)
max_tokens上限按任务定,默认往往偏小
streaming是否流式True 时支持 .stream()
base_url兼容入口DeepSeek=https://api.deepseek.com

你 W1 的 RealLLM 其实就是这个 ChatOpenAI 的封装。所谓"统一厨师证"——不管底层是 DeepSeek 还是通义,接口都长一样,换模型只改两个字段。

invoke / stream / batch —— 三种调用方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 1) invoke:一次拿完整结果(最常用)
msg = model.invoke("你好")          # 返回 AIMessage 对象
print(msg.content)                  # 取文本用 .content

# 2) stream:流式,一个字一个字冒(适合聊天界面)
for chunk in model.stream("写一首诗"):
    print(chunk.content, end="", flush=True)

# 3) batch:一次塞多个输入,并发处理(比循环 invoke 快)
results = model.batch(["1+1=?", "2+2=?"])
print([r.content for r in results])

这三个方法来自 Runnable 协议,后面所有组件(Prompt/Parser/Chain)都支持,这是 LCEL 能串起来的底层原因。


四、Prompt 提示词模板:ChatPromptTemplate

ChatPromptTemplate.from_messages —— 构造消息模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from langchain_core.prompts import ChatPromptTemplate

# 用 (角色, 文本) 元组列表;{q} 是占位变量
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个乐于助人的助手。"),   # system 角色
    ("user", "{q}"),                          # user 角色,{q} 待填
])

# 调用时传字典填充变量
messages = prompt.invoke({"q": "什么是 LCEL?"})
print(messages.to_messages())   # 得到 [SystemMessage, HumanMessage]

你 W1 的 prompt_lib 干的就是这事——ChatPromptTemplate 是它的框架标准版,支持 {变量} 插值。

MessagesPlaceholder —— 历史消息占位

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是有记忆的助手。"),
    MessagesPlaceholder("history"),     # 这里会展开成一串历史消息对象
    ("user", "{question}"),
])

# 调用时 history 必须传"消息对象列表"(不是 dict)
from langchain_core.messages import HumanMessage, AIMessage
chain.invoke({
    "question": "我叫什么?",
    "history": [HumanMessage(content="我叫小明"), AIMessage(content="你好小明")]
})

⚠️ placeholder 要配消息对象列表,不是 dict 列表。你自研记忆返回的是 dict,必须先用 _to_lc 转换(见第八节)。

partial —— 预填部分变量

1
2
3
4
# 有些变量每次都一样(如固定语言),可以提前 partial 掉
prompt = ChatPromptTemplate.from_messages([("system", "用{lang}回答:{q}")])
prompt_zh = prompt.partial(lang="中文")   # 预填 lang,之后只需传 {q}
print(prompt_zh.invoke({"q": "你好"}).to_messages())

五、OutputParser 输出解析器

StrOutputParser —— 把 AIMessage 变成纯字符串

1
2
3
4
5
from langchain_core.output_parsers import StrOutputParser

parser = StrOutputParser()
text = parser.invoke(model.invoke("你好"))   # AIMessage → str
print(type(text))   # <class 'str'>

JsonOutputParser —— 解析 JSON 输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import ChatPromptTemplate

parser = JsonOutputParser()
prompt = ChatPromptTemplate.from_messages([
    ("system", "只输出 JSON,不要解释。{format_instructions}"),
    ("user", "列出三种水果和颜色"),
])

# JsonOutputParser 会给你一段"格式说明"塞进 prompt
chain = prompt | model | parser
data = chain.invoke({"format_instructions": parser.get_format_instructions()})
print(data)   # 已经是 dict,不用自己 json.loads

PydanticOutputParser —— 强类型结构化(进阶)

1
2
3
4
5
6
7
8
9
from pydantic import BaseModel, Field
from langchain_core.output_parsers import PydanticOutputParser

class Person(BaseModel):
    name: str = Field(description="姓名")
    age: int = Field(description="年龄")

parser = PydanticOutputParser(pydantic_object=Person)
# parser.get_format_instructions() 会生成"必须输出符合 Person schema 的 JSON"

你平时手动 json.loads(...),就是 StrOutputParser/JsonOutputParser 的活。框架的价值:连"格式说明"都帮你自动生成。

⚠️ 验收坑StrOutputParser 在某些版本内部可能把结果裹一层包装类型(如旧版的 TextAccessor)。稳健验收写 isinstance(result, str),别断言具体类名——类名会变,str 子类关系不会变。


六、LCEL 链:Runnable 与管道

chain = prompt | model | parser —— 一条最小链

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
import os

prompt = ChatPromptTemplate.from_messages([("system", "你是一个简洁助手"), ("user", "{question}")])
model = ChatOpenAI(model="deepseek-chat", api_key=os.getenv("DEEPSEEK_API_KEY"),
                   base_url="https://api.deepseek.com")
parser = StrOutputParser()

# | 就是传送带:前一个输出自动变成后一个输入
chain = prompt | model | parser

# 调用
print(chain.invoke({"question": "什么是 LCEL?"}))   # 直接拿到 str

工厂流水线类比

  • prompt = 配料机(dict → 消息列表)
  • model = 加工机(消息 → AIMessage)
  • parser = 包装机(AIMessage → 纯字符串)
  • | = 传送带(自动流转)

get_graph().print_ascii() —— 可视化整条链

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
chain.get_graph().print_ascii()
# 输出:
#   PromptInput
#      |
#   ChatPromptTemplate
#      |
#   ChatOpenAI
#      |
#   StrOutputParser
#      |
#   StrOutputParserOutput

排错神器:链不工作时先打印结构图,看数据流向对不对。

Runnable 协议 —— 为什么能 |

所有 LangChain 组件都实现 Runnable 接口,至少有 invoke / stream / batch 三个方法。| 运算符本质是"把左边输出喂给右边输入"。普通 Python 函数没有这接口,必须先包成 RunnableLambda(见第七节)才能入链。


七、Runnable 组合件

RunnableParallel —— 一条输入,并行出多个结果

1
2
3
4
5
6
7
8
9
from langchain_core.runnables import RunnableParallel

# 同一条 question 同时喂给"回答链"和"关键词链"
parallel = RunnableParallel(
    answer=answer_chain,        # 键名 answer 会成为结果 dict 的键
    keywords=kw_chain,
)
res = parallel.invoke({"question": "什么是 LCEL?"})
# res = {"answer": "...", "keywords": "..."}  返回 dict

你 W1-W3 没有"并行"概念,这是框架新增能力:一条输入复制多份,同时跑不同子链。

RunnablePassthrough.assign —— 保留原输入,再追加新字段

1
2
3
4
5
6
7
8
9
from langchain_core.runnables import RunnablePassthrough

# 原输入 {"question": "..."} 被保留,再追加 长度 / 前5字 两个字段
enrich = RunnablePassthrough.assign(
    长度=lambda x: len(x["question"]),
    前5字=lambda x: x["question"][:5],
)
out = enrich.invoke({"question": "什么是LCEL?"})
# out = {"question": "什么是LCEL?", "长度": 7, "前5字": "什么是LC"}

这等价于字典解包 {**x, "新字段": ...}——你 W3 get_context 里"保留原消息 + 插新消息"的思路,框架用 .assign 标准化了。

RunnableLambda —— 任意 Python 函数入链(适配器)

1
2
3
4
5
6
7
from langchain_core.runnables import RunnableLambda

def 清洗(text: str) -> str:
    return " ".join(text.split())[:50]     # 普通函数,参数类型要对接上一个 Runnable 的输出

chain = answer_chain | RunnableLambda(清洗)   # 把函数包成 Runnable,才能用 |
print(chain.invoke({"question": "介绍 LangChain"}))

类比"插头转换器":普通函数是两脚插头,LangChain 管道是三脚插座,套个 RunnableLambda 就通了。这也是你把 W3 自研 MemoryManager / compress() 接进链的桥

RunnableBranch —— 条件分支(按判断走不同子链)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from langchain_core.runnables import RunnableBranch

branch = RunnableBranch(
    (lambda x: "天气" in x["question"], weather_chain),   # 条件1 成立 → 天气链
    (lambda x: "计算" in x["question"], calc_chain),      # 条件2 成立 → 计算链
    default_chain,                                        # 都不成立 → 默认链(必须放最后)
)

for q in ["北京天气?", "计算 12*8", "你好"]:
    print(branch.invoke({"question": q}))

条件短路:从上到下匹配,命中第一个就走,所以默认链必须置底。这是 W5 LangGraph 条件 edge 的雏形。


八、Memory 记忆

三种记忆模式

模式存啥对应你 W3
短期(窗口)最近 N 轮对话ShortTermMemory
长期(向量)用户事实 / 知识LongTermMemory + Chroma
压缩老历史 → 一条摘要compress.py

RunnableLambda 接入自研 MemoryManager(重点)

你的 MemoryManager.get_context() 返回 dict 列表,但 prompt 的 MessagesPlaceholder消息对象列表。中间必须有一道"格式转换"——这就是 _to_lc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langchain_core.runnables import RunnableLambda

# 桥的包装器:dict 消息 → LangChain BaseMessage
def _to_lc(m: dict):
    role, content = m.get("role", "user"), m.get("content", "")
    if role == "system":
        return SystemMessage(content=content)
    if role == "assistant":
        return AIMessage(content=content)
    return HumanMessage(content=content)

# 接入 W3 自研 MemoryManager(失败则降级最小窗口版)
try:
    from memory.memory_manager import MemoryManager
    _mm = MemoryManager(window=6, max_tokens=500)

    def get_history(x):
        # 真实版:query=本轮问题 → 长期召回 + 滚动窗口
        return [_to_lc(m) for m in _mm.get_context(query=x.get("question"))]
except Exception as e:
    print(f"[降级]未找到 memory 包({e}),使用最小窗口版")
    class _MiniMM:
        def __init__(self, window=6):
            self.buf, self.window = [], window
        def get_context(self, query=None):
            return self.buf[-self.window:]
        def add_user(self, t):
            self.buf.append({"role": "user", "content": t})
        def add_ai(self, t):
            self.buf.append({"role": "assistant", "content": t})
    _mm = _MiniMM()
    def get_history(x):
        return [_to_lc(m) for m in _mm.get_context()]

# 链首用 RunnableLambda 加料:保留 question,追加 history 字段
load_mem = RunnableLambda(lambda x: {**x, "history": get_history(x)})
chain = load_mem | prompt | model | StrOutputParser()

def ask(question: str) -> str:
    ans = chain.invoke({"question": question})
    if hasattr(_mm, "add_user"):
        _mm.add_user(question); _mm.add_ai(ans)
    else:
        _mm.add({"role": "user", "content": question})
        _mm.add({"role": "assistant", "content": ans})
    return ans

for q in ["我叫小明,喜欢打篮球", "我刚说的爱好是什么?", "我名字叫什么?"]:
    print(f"Q: {q}\nA: {ask(q)}\n")

⚠️ 相对导入坑:上面的 from memory.memory_manager import ... 直接 python lc_memory.py 跑会炸(脚本被当顶层模块,.. / 包路径解析不了)。修法有二:① sys.path.insert(0, 项目根目录) + 绝对导入;② 用 python -m 包名.模块名 启动。你 W3 调试时踩过同款坑,这里是标准解法。

关键设计:MemoryManager 不重写,只包一层——延续你 W3 的"调度者不重写逻辑"原则。get_context / compress 原样复用,框架只是多了一个入口适配器。


九、Retriever 检索与 RAG

RAG 流程:文档 → 切分 → 向量化 → 存入 → 检索 → 拼进 prompt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 1) 切分文档
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
docs = splitter.create_documents([open("知识库.txt", encoding="utf-8").read()])

# 2) 向量化 + 存入 Chroma(你 W3 的 Chroma 长期记忆就是它)
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
vectordb = Chroma.from_documents(docs, embeddings, persist_directory="./chroma_db")

# 3) 检索器:query → 相关片段
retriever = vectordb.as_retriever(search_kwargs={"k": 3})
hits = retriever.invoke("退款政策是什么?")
print([d.page_content for d in hits])

你 W3 的"长期向量记忆"和 RAG 的"知识检索"是同一个东西——Chroma 接一次复用,不用另起炉灶。

接进链(检索增强问答)

1
2
3
4
5
6
7
from langchain_core.runnables import RunnablePassthrough

# 用 .assign 把检索结果拼进上下文,原 question 保留
rag_chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | rag_prompt | model | parser
)

十、Tool 工具与 @tool

@tool —— 把函数变成模型可调用的工具

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from langchain_core.tools import tool

@tool
def calculator(expr: str) -> str:
    """计算一个数学表达式。"""
    return str(eval(expr))

@tool
def get_weather(city: str) -> str:
    """查询某城市天气。"""
    return f"{city} 今天晴,25℃"

# 模型能"看到"工具的描述和参数,自己决定调哪个
tools = [calculator, get_weather]
model_with_tools = model.bind_tools(tools)

你 W2 的 fc_loop 手写了"拼 tool schema + 解析 tool_calls + 循环"。@tool 装饰器把函数自动变成标准接口,省掉手写 schema。

bind_tools + 工具调用循环

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from langchain_core.messages import HumanMessage, ToolMessage

msg = model_with_tools.invoke("北京天气怎么样?计算 12*8")
# msg.tool_calls 里有模型想调的工具和参数
for call in msg.tool_calls:
    fn = {"calculator": calculator, "get_weather": get_weather}[call["name"]]
    result = fn.invoke(call["args"])          # 执行工具
    # 把结果回灌给模型,让它继续决定下一步(这就是 Agent 循环)
    follow = model_with_tools.invoke([
        HumanMessage("北京天气怎么样?计算 12*8"),
        msg,
        ToolMessage(content=str(result), tool_call_id=call["id"]),
    ])

十一、Agent 智能体

本质:模型决定调哪个工具 → 执行 → 结果喂回 → 再决定

你 W2 的 fc_loop 就是这个循环的标准化版本。LangChain 提供 create_react_agent 等高级封装:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from langchain.agents import create_react_agent, AgentExecutor

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个助手,可用工具完成任务。"),
    ("user", "{input}"),
    ("assistant", "{agent_scratchpad}"),   # 思考轨迹占位
])
agent = create_react_agent(model_with_tools, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

print(executor.invoke({"input": "北京天气怎么样?再算下 12*8"})["output"])

AgentExecutor 帮你把"调模型 → 解析 tool_calls → 执行 → 回灌"的循环跑完。你自研的 fc_loop 逻辑完全等价,只是它更通用。


十二、Callbacks 回调

监听链路每一步事件(调试 / 日志 / 计费)

1
2
3
4
from langchain_core.callbacks import StdOutCallbackHandler

# verbose 等价写法:把回调传进 invoke
chain.invoke({"question": "你好"}, config={"callbacks": [StdOutCallbackHandler()]})

生产环境常用自定义 callback 统计 token 消耗、记录每次 LLM 调用——对应你 Vibe Coding 手册里"操作透明、禁止黑盒"的纪律。


十三、LangGraph 状态图(进阶)

StateGraph —— 把流程编排成"有状态 + 可有环"的图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator

class State(TypedDict):
    question: str
    answer: str
    steps: Annotated[list, operator.add]   # 累加型字段,每步追加

def call_model(state: State):
    state["answer"] = chain.invoke({"question": state["question"]})
    state["steps"].append("call_model")
    return state

def should_continue(state: State):
    return END if "结束" in state["answer"] else "call_model"   # 条件边

graph = StateGraph(State)
graph.add_node("call_model", call_model)
graph.add_edge("call_model", "call_model")          # 自环:可多轮
graph.add_conditional_edges("call_model", should_continue)
app = graph.compile()

print(app.invoke({"question": "你好", "steps": []}))

LangGraph = LCEL 的"有环升级版"。你 W4 学的 RunnableLambda(node)、RunnableBranch(conditional edge)在这里成了 add_node / add_conditional_edges 的一等公民。W5 你会把手写 Agent 的循环搬进状态图


十四、常用技巧与踩坑

技巧 1:流式 + 解析器组合

1
2
for chunk in chain.stream({"question": "数到5"}):
    print(chunk, end="", flush=True)   # StrOutputParser 保证每块是 str

技巧 2:batch 并发提速

1
results = chain.batch([{"question": "1+1"}, {"question": "2+2"}])   # 比循环 invoke 快

技巧 3:降级保护

生产代码用 try/except 包住导入,导入失败就退回最小实现(见第八节 _MiniMM),保证程序不崩。

踩坑 1:相对导入直接跑脚本会炸 ⚠️

from ..memory import Xpython xxx.py 下必然失败。改用 sys.path.insert(0, 根目录) + 绝对导入,或 python -m 包.模块

踩坑 2:版本弃用 ⚠️

旧教程的 LLMChainConversationChaininitialize_agent 已弃用。坚持用 LCEL(core)+ create_react_agent + LangGraph

踩坑 3:parser 输出类型 ⚠️

验收用 isinstance(result, str),别写死类名(跨版本包装层会变)。

踩坑 4:流式不能用普通函数截断 ⚠️

RunnableLambda 包的函数在 .stream() 下要支持流式透传,否则丢字。简单场景用 StrOutputParser 最稳。


十五、小结

LangChain 看着庞大,日常 80% 的场景只用这几样:ChatPromptTemplate 拼提示 + ChatOpenAI 调模型 + StrOutputParser 解析 + | 串成链 + RunnableLambda 接你的老代码。先把这条主线跑熟,记忆/检索/工具/Agent 都是往这条主线上挂模块。

回顾你 W1-W3 的手写轮子,对应关系是:

你自研的LangChain 标准件
RealLLM / llm_clientChatOpenAI
prompt_libChatPromptTemplate
手动取 .content / json.loadsStrOutputParser / JsonOutputParser
fc_loop 工具循环Agent / bind_tools / ToolMessage
MemoryManagerMemory + RunnableLambda 接入
Chroma 长期记忆VectorStore / Retriever
tools/ 注册表@tool 装饰器

框架化不是学新东西,是把手动接线变成标准管道。 你已有的底层认知(token、记忆、压缩、工具循环)全部复用,只是换了更通用的语法。

下一篇建议深入 LangGraph:把 Agent 的多轮循环、条件分支、状态共享,用状态图标准化——那是 W5 的主战场,也是你手写 fc_loop / MemoryManager 的自然归宿。

记住一条铁律:凡是接你自研代码的,先想"它实现 Runnable 了吗?没实现就包一层 RunnableLambda";凡是接 dict 消息的,先想"prompt 要的是消息对象吗?要就过一道 _to_lc"。这两道桥通了,LangChain 的大门就彻底打开了。

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计