F02A:从零开始的 ai agent 学习 - 进阶篇
F02A:从零开始的 ai agent 学习 - 进阶篇
F02A(From Zero To Agent)是我个人从基本为零的 ai 基础开始独立学习到编写 Agent 的过程记录。
文章更像是一个学习记录而非教程,因此难免会出现一些偏差或错漏,读者多多担待
Stage 3
LangChain 基础
LangChain 是一个用于构建 基于大语言模型(LLM)的应用程序框架。它提供一系列的组件,帮助开发者把像 GPT、Claude、Llama 等模型与 数据、工具、数据库和应用逻辑连接起来,从而开发更复杂的 AI 应用
它解决了直接 api 调用过程中代码难以复用和管理的问题,避免使用很多的胶水代码进行 agent 构建和扩展
LangChain 更适合基于高层抽象快速构建 LLM 应用和常见 agent。对于需要更细粒度控制的复杂流程,例如带有状态管理、条件分支、循环、持久化和人工介入的 agent 系统,通常更适合使用 LangGraph。
你可以通过文档学习其详细的语法和功能 LangChain 中文文档
文档中没有出现 LCEL(LangChain Expression Language)相关的内容,这是因为新版 Langchain 中,官方更加看重基于 LangGraph 的使用 create_agent 的架构,但是 LCEL 仍然是需要我们理解的
LCEL 非常符合 LangChain 的“链式调用”直觉,先描述流程,声明组合,再统一执行
from langchain_core.prompts import ChatPromptTemplate
from common import get_chat_model
def main() -> None:
model = get_chat_model()
prompt = ChatPromptTemplate.from_template(
"你是一名 LangChain 学习助手。请用 3 条中文要点解释 {topic},每条不超过 20 个字。"
)
chain = prompt | model #LCEL
response = chain.invoke({"topic": "LCEL 的作用"})
print("=== Model + Prompt Demo ===")
print(response.content)
if __name__ == "__main__":
main()
可以将 LCEL 的管道符 | 理解成将前一个参数的输出传递到后一个参数
为了方便各种参数的使用,我们定义一些工具方法
from __future__ import annotations
import os
from pathlib import Path
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
BASE_DIR = Path(__file__).resolve().parents[1]
DATA_DIR = BASE_DIR / "data" / "knowledge"
VECTOR_DIR = BASE_DIR / ".chroma"
def load_env() -> None:
load_dotenv(BASE_DIR / ".env")
def require_env(name: str) -> str:
value = os.getenv(name)
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
return value
def get_chat_model(temperature: float = 0.2) -> ChatOpenAI:
load_env()
return ChatOpenAI(
api_key=require_env("OPENAI_API_KEY"),
base_url=require_env("OPENAI_BASE_URL"),
model=require_env("OPENAI_MODEL"),
temperature=temperature,
)
def get_embeddings() -> OpenAIEmbeddings:
load_env()
return OpenAIEmbeddings(
api_key=require_env("OPENAI_API_KEY"),
base_url=require_env("OPENAI_BASE_URL"),
model=require_env("OPENAI_EMBEDDING_MODEL"),
)
这样就可以使用 dotenv 来方便的传参,不用多次修改了
OPENAI_API_KEY=sk-...
OPENAI_BASE_URL=https://.../v1
OPENAI_MODEL=gpt-5.2
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
如果你的第三方 api 没有 embedding 模型,可以尝试本地部署一个
from langchain_huggingface import HuggingFaceEmbeddings
def get_embeddings():
return HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
from langchain_ollama import OllamaEmbeddings
def get_embeddings():
return OllamaEmbeddings(model="locusai/all-minilm-l6-v2")
结构化输出
结构化输出允许我们自定义模型的输出格式,这样我们就不需要对模型输出进行自然语言处理,就可以得到方便的结构化数据供应用使用
from pydantic import BaseModel, Field
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from common import get_chat_model
class StudyPlan(BaseModel):
topic: str = Field(description="学习主题")
difficulty: str = Field(description="难度级别")
next_action: str = Field(description="下一步行动")
def plain_text_demo() -> None:
model = get_chat_model()
prompt = ChatPromptTemplate.from_template(
"把 {topic} 解释成一句适合初学者理解的话。"
)
chain = prompt | model | StrOutputParser()
result = chain.invoke({"topic": "LangChain 中的 Runnable"})
print("=== LCEL + StrOutputParser ===")
print(result)# 返回plain text形式
# 如果不使用StrOutputParser(),得到的result是类似AIMessage(content="...")的形式
def structured_output_demo() -> None:
model = get_chat_model(temperature=0)
structured_model = model.with_structured_output(StudyPlan)
prompt = ChatPromptTemplate.from_template(
"你是学习规划助手。根据主题 {topic} 生成一个简洁学习建议。"
)
chain = prompt | structured_model
result = chain.invoke({"topic": "RAG 入门"})# result 的类型为StudyPlan
print("=== Structured Output Demo ===")
print(result.model_dump_json(indent=2))
def main() -> None:
plain_text_demo()
print()
structured_output_demo()
if __name__ == "__main__":
main()
RAG
大型语言模型 (LLM) 功能强大,但它们有两个主要的局限性:
- 有限的上下文 — 它们无法一次性消化整个语料库。
- 静态的知识 — 它们的训练数据是固定在某个时间点的。
检索通过在查询时获取相关的外部知识来解决这些问题。这是检索增强生成 (Retrieval-Augmented Generation, RAG) 的基础:用特定于上下文的信息来增强 LLM 的答案。
langchain 使用 document 数据结构来处理外部数据
Document(
page_content="文本内容",
metadata={"source": "file.pdf"}
)
一个典型的检索流程大致如下

- Document Loader
documentLoader 的作用是将外部来源的不同格式的各种信息转化为统一的 document 对象
- Splitter
Splitter 切分器的作用是将原始文档切成块(chunks),常见的切分方式按固定长度切分、按段落切分、按标题层级切分、按句子切分等等,切分的目的是让知识变成更细粒度、可检索、可送给模型的上下文片段,解决检索全篇文档时向量语义过于平均和上下文过长的问题
- Embedding
接触过自然语言处理的话对嵌入应该不陌生,它的作用是将不同的 chunk 转化为向量,也就是将文本变为可计算语义相似度的数值表示,方便后续机器通过语义进行检索
- Vector Store
把所有 chunk 的 embedding 存起来,并建立索引,通过其数据结构来支持高效相似度搜索。它被检索器 Retriever 封装后,就可以实现传入 query,传出文档内容
这里用一些简单的 txt 作为示例,使用 ChromaDB 作为 Vector DB 来做向量语义检索
from pathlib import Path
from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
from common import DATA_DIR, VECTOR_DIR, get_embeddings
def load_text_documents() -> list[Document]:
documents: list[Document] = []
for path in sorted(DATA_DIR.glob("*.txt")):
text = path.read_text(encoding="utf-8")
documents.append(Document(page_content=text, metadata={"source": path.name}))
return documents
def main() -> None:
raw_documents = load_text_documents()
splitter = RecursiveCharacterTextSplitter(chunk_size=220, chunk_overlap=40)
chunks = splitter.split_documents(raw_documents)
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=get_embeddings(),
persist_directory=str(VECTOR_DIR),
)
print("=== RAG Indexing Demo ===")
print(f"Raw documents: {len(raw_documents)}")
print(f"Chunks: {len(chunks)}")
print(f"Persist directory: {Path(VECTOR_DIR)}")
if __name__ == "__main__":
main()
这会生成一个.chroma 文件夹,文件夹下是搭建好的 chromadb 向量数据库
可以使用检索器来匹配数据库内容
from operator import itemgetter
from langchain_chroma import Chroma
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda
from common import VECTOR_DIR, get_chat_model, get_embeddings
# 将多个document格式化为字符串
def format_docs(docs) -> str:
s = "\n\n".join(doc.page_content for doc in docs)
# 查看查询到的上下文
print(s)
return s
def main() -> None:
# 加载 vector db
vectorstore = Chroma(
persist_directory=str(VECTOR_DIR),
embedding_function=get_embeddings(),
)
# 创建检索器 k表示返回k个相关文档
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
prompt = ChatPromptTemplate.from_template(
"""你只能基于提供的上下文回答问题。
上下文:
{context}
问题:{question}
如果上下文不足,请明确说“我需要更多资料”。"""
)
# 将检索文档和用户查询构建为新的输入字典传入prompt
# RunnableLambda的作用是将函数包装为runnable接口,用于结构化输出
chain = (
{
"context": itemgetter("question") | retriever | RunnableLambda(format_docs),
"question": itemgetter("question"),
}
| prompt
| get_chat_model()
| StrOutputParser()
)
question = "RAG 的基础流程包含哪些步骤?"
# 原始输入字典为{"question": question},itemgetter("question")能够获取到这里的值
answer = chain.invoke({"question": question})
print("=== RAG Chain Demo ===")
print(f"Question: {question}")
print(answer)
if __name__ == "__main__":
main()

消息历史
记忆(memory),是保证 agent 能够保持上下文一致性,避免重复操作的关键。
消息历史(message history)是一种短期记忆。在此次前的模块代码中,短期记忆通过上下文 prompt 可以进行管理,在 langchain 中则是提供了一些接口,用于更加方便灵活地控制它
在 langchain classic 的 chain 架构中,我们使用 ConversationChain 来做这件事
from langchain_classic.chains import ConversationChain
from langchain_classic.memory import ConversationBufferMemory
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
# 一个链实例 + 一份 memory
conversation = ConversationChain(
llm=llm,
memory=ConversationBufferMemory(return_messages=True),
verbose=True,
)
# 用户 A
resp1 = conversation.invoke({"input": "我叫张三"})
print("A1:", resp1["response"])
# 用户 B
resp2 = conversation.invoke({"input": "我叫李四"})
print("B1:", resp2["response"])
# 用户 A 再问
resp3 = conversation.invoke({"input": "我刚才叫什么名字?"})
print("A2:", resp3["response"])
在这种情况下,我们通过构造一条专门的链子来实现消息历史,这也就意味着一条链子会强制绑定一个 memory,所以当我们需要进行多会话时就可能产生混淆
要解决这个问题,我们需要将消息历史独立在链外,最好能够方便地将不同的 session 通过某种机制分离开来,而这就是 RunnableWithMessageHistory 做的事
Runnable 指的是这个类实现了 runnable 接口,在 langchain 中,runnable 接口提供了一系列触发方法,包括 invoke,batch 等等。容易理解到,我们构造出的 chain 实际上都是 runnable 的。事实上链上的每一个部分也都是 runnable 的
RunnableWithMessageHistory 提供了一个构造方法,将传入的 chain 包装另一个 runnable,这个包装层就负责管理 chat message history
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from common import get_chat_model
store: dict[str, InMemoryChatMessageHistory] = {} # session管理容器
def get_session_history(session_id: str) -> InMemoryChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
def main() -> None:
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一名简洁的学习助手。基于已有对话继续回答。"),
MessagesPlaceholder(variable_name="history"),
("human", "{input}"),
]
)
chain = prompt | get_chat_model()
chat = RunnableWithMessageHistory(
runnable=chain, # 被包装runnable
get_session_history=get_session_history, # 工厂方法,传入sessionid返回消息历史对象
input_messages_key="input", # 用户消息字段
history_messages_key="history", # 历史消息字段
)
config = {"configurable": {"session_id": "demo-user"}}
first = chat.invoke({"input": "我在学 LangChain,现在重点是 RAG。"}, config=config)
second = chat.invoke({"input": "请根据刚才的重点,给我一个下一步建议。"}, config=config)
print("=== Message History Demo ===")
print("First turn:")
print(first.content)
print()
print("Second turn:")
print(second.content)
if __name__ == "__main__":
main()
代码中我们发现 session 是通过一个 config 对象传入的,这实际上是 LangGraph 进行持久化的手段之一
agent tool
此前的例子中,我们依然使用 LCEL 语法进行 agent 构造,但实际上 Langchain v1 文档中更加推行的是使用 create_agent。它方便快捷地产生一个可用 agent,使用 LangGraph 构建了一个基于图的智能体。图由节点(步骤)和边(连接)组成,定义了智能体如何处理信息。智能体在图中移动,执行节点,如模型节点(调用模型)、工具节点(执行工具)或中间件。
可以发现,这个方法产生的 agent 由模型去指定流程,而不是由我们显式地声明流程,这是它与 LCEL 一个巨大的区别,它会一直运行工具以实现目标,直到满足停止条件——即模型输出最终结果或达到迭代次数限制。
用文档中的一句话来说
create_agent提供了一个生产就绪的智能体实现。
agent 内部并不排斥你把某些工具、子流程做成 Runnable 或链;只是对外层来说,create_agent 负责工具循环和 agent 状态管理。它也能更加方便地进行工具调用,因为它可以由模型根据已知信息去决定是否调用或重复调用工具
创建工具最简单的方法是使用 @tool 装饰器。默认情况下,函数的文档字符串(docstring)会成为工具的描述,帮助模型理解何时使用它:
@tool
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"{city} is sunny."
官方 docs 明确强调 tool 是“well-defined inputs and outputs”。因此我们函数的输入输出参数必须定义清楚,才能让模型生成正确的结构化参数
如果需要,我们可以使用 Pydantic 模型 或 JSON 架构 定义复杂的输入
from pydantic import BaseModel, Field
from langchain.tools import tool
class SearchInput(BaseModel):
query: str = Field(description="The search query")
top_k: int = Field(default=3, description="Number of results to return")
@tool(args_schema=SearchInput)
def search_docs(query: str, top_k: int = 3) -> str:
"""Search documents."""
return f"query={query}, top_k={top_k}"
一个简单的 agent tool 调用的 demo
from langchain.agents import create_agent
from langchain_core.tools import tool
from common import get_chat_model
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two integers."""
return a * b
@tool
def study_tip(topic: str) -> str:
"""Return a short study suggestion for a LangChain topic."""
tips = {
"rag": "先确认检索结果质量,再优化提示词。",
"agent": "先保证工具描述清晰,再观察模型调用行为。",
"lcel": "先画出数据流,再写 runnable 组合。",
}
return tips.get(topic.lower(), "先写最小示例,再逐步加复杂度。")
def main() -> None:
agent = create_agent(
model=get_chat_model(),
tools=[multiply, study_tip],
system_prompt="你是一名 LangChain 助手。能直接回答就直接回答,需要精确计算或建议时调用工具。",
)
result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": "45 乘以 13 等于多少?另外给我一条关于 RAG 的学习建议。",
}
]
}
)
print("=== Agent Tools Demo ===")
for message in result["messages"]:
pretty = getattr(message, "pretty_repr", None)
if callable(pretty):
print(pretty())
else:
print(message)
if __name__ == "__main__":
main()
=== Agent Tools Demo ===
================================ Human Message =================================
45 乘以 13 等于多少?另外给我一条关于 RAG 的学习建议。
================================== Ai Message ==================================
Tool Calls:
multiply (call_NrB2dxjtVrIR182fW0sgLUx0)
Call ID: call_NrB2dxjtVrIR182fW0sgLUx0
Args:
a: 45
b: 13
study_tip (call_Gz9x47No717dKBPjI6EzfuoO)
Call ID: call_Gz9x47No717dKBPjI6EzfuoO
Args:
topic: RAG
================================= Tool Message =================================
Name: multiply
585
================================= Tool Message =================================
Name: study_tip
先确认检索结果质量,再优化提示词。
================================== Ai Message ==================================
45 × 13 = **585**。
RAG 学习建议:**先把“检索”做扎实**——用一套固定问题集评估召回率/相关性(embedding、chunk 切分、top-k、重排),确认检索结果足够好后,再去优化提示词与生成部分。
Stage 4
在 Stage 3 中,我们学习了单链路、RAG、消息历史和工具 Agent。到 Stage 4,重点不再是“怎么再多加一个工具”,而是“什么时候应该拆成多个 Agent,由谁协作、谁路由、谁校验”。基于这种更复杂的流程控制需求,我们需要学习一个更底层的 agent 编排与运行时,它就是 LangGraph
有关 LangChain 和 LangGraph 的关系,我们可以理解为:LangChain 是 LangGraph 的上层封装,LangGraph 是 LangChain 的底层引擎。正如我们之前提到的那样, LangChain 的 create_agent 本身就是用 LangGraph 的 graph runtime 实现的
推荐阅读书籍:《Agentic Design Patterns》中文版
本书系统梳理了智能体(Agent)设计领域的 21 种常见模式,涵盖提示链、路由、并行化、反思、工具使用、规划、多智能体协作、记忆管理等内容,并配有可运行的示例代码与配图说明,适合不同背景的读者深入理解和实践。
本章节环境准备与 Stage3 相同,但大部分示例不依赖 embeddings
Supervisor 模式
在单一 agent 的设计模式中,我们将所有的任务都交由一个 agent 去完成。这导致 agent 的角色一会可能是 coder,一会又是 writer,它的 prompt 和状态会迅速膨胀,角色会混杂。这导致 agent 可能对这些任务都草率了事,降低任务完成的质量。因此将子 agent 按能力边界设计,将不同的任务发给不同的 subagent 进行,能够很好的提高质量和控制成本。
在 Supervisor 模式中,主 agent 起到一个监督者的作用。由一个 supervisor 负责判断、分派、控制通信流,并协调多个专门 worker agent 完成任务。这种中心化编排的特点使其能够较好地拆解复杂任务,将计划与执行分离,从而从而处理单 agent 不擅长的复合任务。
from langchain.agents import create_agent
from langchain_core.tools import tool
from common import get_chat_model
planner_agent = create_agent(
model=get_chat_model(),
system_prompt="你是规划子 Agent。把任务拆成 3 个可执行步骤,回答简洁。",
)
critic_agent = create_agent(
model=get_chat_model(),
system_prompt="你是风险检查子 Agent。指出计划里最可能失败的 2 个点。",
)
def extract_last_text(result: dict) -> str:
message = result["messages"][-1]
return getattr(message, "content", str(message))
@tool
def ask_planner(task: str) -> str:
"""Ask the planning subagent to break a task into steps."""
result = planner_agent.invoke({"messages": [{"role": "user", "content": task}]})
return extract_last_text(result)
@tool
def ask_critic(plan_text: str) -> str:
"""Ask the critic subagent to inspect a plan and identify risks."""
result = critic_agent.invoke({"messages": [{"role": "user", "content": plan_text}]})
return extract_last_text(result)
def main() -> None:
supervisor = create_agent(
model=get_chat_model(),
tools=[ask_planner, ask_critic],
system_prompt=(
"你是 supervisor。面对用户任务时,先调用规划 Agent,"
"再调用风险检查 Agent,最后给出整合后的建议。"
),
)
result = supervisor.invoke(
{
"messages": [
{
"role": "user",
"content": "帮我规划一个两天内完成的 LangGraph 多 Agent 学习安排。",
}
]
}
)
print("=== Subagents Supervisor Demo ===")
print(extract_last_text(result))
if __name__ == "__main__":
main()
在这个例子中,我们实际并没有实现所谓的 agent 间的交互,而是使用了一种将 subagent 封装为主 agent 的工具的方式。这启示我们:多 Agent 最小实现不一定需要完整图结构。
在这种模式中,中心 agent 可能会成为瓶颈,每多一层编排,就多一层 latency 和 token/推理开销。另外,如果 agent 划分过细、能力边界高度重叠,反而会让 supervisor 更难判断。因此不要把高度相似的 agents 长期分开,否则会降低编排/分类效果。
Router 模式
Router 模式和 Supervisor 有相似之处,两者都是将任务分给能力边界划分的 subagent 去执行处理。区别在于 Router 模式的中心节点核心职责是分流,先判断“这个请求该交给谁”,然后把任务发给某个 agent / workflow。通常自己不深度参与后续推理,偏 intent classification / dispatch。而 Supervisor 还会收集 subagent 返回的结果,整理分析,必要时再做委派。
在这个例子中,我们将看到 graph 是如何构建的
from typing import Literal
from langchain.agents import create_agent
from langgraph.graph import END, START, StateGraph
from pydantic import BaseModel, Field
from typing_extensions import TypedDict
from common import get_chat_model
class RouteDecision(BaseModel): # 结构化输出:指定一个subagent
route: Literal["research", "writing", "analysis"] = Field(
description="Best route for the request"
)
class RouterState(TypedDict, total=False): # 图形的状态
task: str
route: str
result: str
research_agent = create_agent(
model=get_chat_model(),
system_prompt="你是 research agent。给出信息搜集方向和关键事实点。",
)
writing_agent = create_agent(
model=get_chat_model(),
system_prompt="你是 writing agent。输出简洁、成稿式的内容。",
)
analysis_agent = create_agent(
model=get_chat_model(),
system_prompt="你是 analysis agent。输出结构化分析和判断依据。",
)
def agent_text(agent, task: str) -> str:
result = agent.invoke({"messages": [{"role": "user", "content": task}]})
return result["messages"][-1].content
def route_task(state: RouterState) -> RouterState:
router = get_chat_model(temperature=0).with_structured_output(RouteDecision)
decision = router.invoke(
f"将任务路由到 research、writing、analysis 之一:{state['task']}"
)
return {"route": decision.route}
def do_research(state: RouterState) -> RouterState:
return {"result": agent_text(research_agent, state["task"])}
def do_writing(state: RouterState) -> RouterState:
return {"result": agent_text(writing_agent, state["task"])}
def do_analysis(state: RouterState) -> RouterState:
return {"result": agent_text(analysis_agent, state["task"])}
def next_node(state: RouterState) -> str:
return state["route"]
def build_graph():
graph = StateGraph(RouterState)
graph.add_node("route_task", route_task)
graph.add_node("research", do_research)
graph.add_node("writing", do_writing)
graph.add_node("analysis", do_analysis)
graph.add_edge(START, "route_task")
graph.add_conditional_edges("route_task", next_node)
graph.add_edge("research", END)
graph.add_edge("writing", END)
graph.add_edge("analysis", END)
return graph.compile()
def main() -> None:
app = build_graph()
result = app.invoke({"task": "请分析多 Agent 相比单 Agent 的主要收益和代价。"})
print("=== Router Graph Demo ===")
print(f"Route: {result['route']}")
print(result["result"])
if __name__ == "__main__":
main()
在构建状态图之前,我们首先需要构建状态
class RouterState(TypedDict, total=False):
task: str
route: str
result: str
状态的作用是在整张图运行时作为一个不断传递和积累的上下文,这样一来一方面所有的中间状态都能得以保存,另一方面也为节点之间解耦,实现多步流程做基础。
构建图时,我们需要构建每一个节点(node)和边(edge)
def build_graph():
graph = StateGraph(RouterState)
graph.add_node("route_task", route_task)
graph.add_node("research", do_research)
graph.add_node("writing", do_writing)
graph.add_node("analysis", do_analysis)
graph.add_edge(START, "route_task")
graph.add_conditional_edges("route_task", next_node)
graph.add_edge("research", END)
graph.add_edge("writing", END)
graph.add_edge("analysis", END)
return graph.compile()
第一个节点通常是模型节点,也就是连接 llm、决定是否使用工具的节点;同时还可以定义一系列的工具节点,这些工具节点可能是包装成工具的 subagent
节点可以视作一个函数,它接收状态的输入,并输出一个字典,表示对状态的更新。
随后定义的边表示 agent 的运行路径,当需要通过条件函数决定下一个节点时,使用 add_conditional_edges。在这里使用了一个 next_node 函数实现路由机制
不难发现,路由决策也是状态的一部分
Parallel 模式
有时一个任务其实包含多个可以同时执行的子任务,而不是一个接一个地串行处理。这时,并行化设计模式就变得至关重要。
并行化模式与 supervisor 模式一样,都属于“中央协调者 + 专门执行者”(orchestrator-worker)的架构。
from operator import add
from typing import Annotated
from langchain.agents import create_agent
from langgraph.graph import END, START, StateGraph
from langgraph.types import Send
from pydantic import BaseModel, Field
from typing_extensions import TypedDict
from common import get_chat_model
class SectionPlan(BaseModel):
sections: list[str] = Field(description="Sections to assign to workers")
class ParallelState(TypedDict, total=False):
topic: str
sections: list[str]
completed_sections: Annotated[list[str], add]
final_report: str
worker_agent = create_agent(
model=get_chat_model(),
system_prompt="你是研究 worker。针对分配给你的章节输出 3 到 4 句高密度总结。",
)
editor_agent = create_agent(
model=get_chat_model(),
system_prompt="你是总编 Agent。把多段材料收敛成一份清晰总结。",
)
def plan_sections(state: ParallelState) -> ParallelState:
planner = get_chat_model(temperature=0).with_structured_output(SectionPlan)
result = planner.invoke(
f"为主题生成 3 个研究小节标题,返回 JSON:{state['topic']}"
)
return {"sections": result.sections}
def fan_out(state: ParallelState) -> list[Send]:
return [Send("worker", {"topic": state["topic"], "section": section}) for section in state["sections"]]
def worker(state: dict) -> ParallelState:
result = worker_agent.invoke(
{
"messages": [
{
"role": "user",
"content": f"主题:{state['topic']}\n小节:{state['section']}",
}
]
}
)
text = result["messages"][-1].content
return {"completed_sections": [f"## {state['section']}\n{text}"]}
def synthesize(state: ParallelState) -> ParallelState:
joined = "\n\n".join(state["completed_sections"])
result = editor_agent.invoke(
{
"messages": [
{
"role": "user",
"content": f"请整理以下并行研究结果,形成最终总结:\n\n{joined}",
}
]
}
)
return {"final_report": result["messages"][-1].content}
def build_graph():
graph = StateGraph(ParallelState)
graph.add_node("plan_sections", plan_sections)
graph.add_node("worker", worker)
graph.add_node("synthesize", synthesize)
graph.add_edge(START, "plan_sections")
graph.add_conditional_edges("plan_sections", fan_out, ["worker"])
graph.add_edge("worker", "synthesize")
graph.add_edge("synthesize", END)
return graph.compile()
def main() -> None:
app = build_graph()
result = app.invoke({"topic": "LangGraph 多 Agent 系统的设计原则"})
print("=== Parallel Research Graph Demo ===")
print(result["final_report"])
if __name__ == "__main__":
main()
在并行化架构中,除了一个初始的协调角色,我们还可以定义一个负责最终收敛的角色。这么做一方面也是可以防止角色漂移,另一方面独立的收敛 agent 可以更好地处理冲突的并行结果。
在这个示例中,我们的图是这样的

图中的虚线代表了 fan_out 方法,这个方法调用了 send 方法,将每一个 section 都进行了一次 send,从而分发给多个 worker,这也是这个示例中的并行部分
def fan_out(state: ParallelState) -> list[Send]:
return [Send("worker", {"topic": state["topic"], "section": section}) for section in state["sections"]]
Send 方法的作用是:给某个节点发送一个自定义 state/input,用于在下一步动态调用这个节点。
graph.add_conditional_edges("plan_sections", fan_out, ["worker"])
配合构建图时的行为,这里完整的调用链即是:
plan_sections读取 state,生成 3 个 section(暂且列为[a,b,c])- 调用
fan_out这个条件函数,返回得到list[Send],这里的["worker"]是一个path_map,他在这里表示声明可能到达的目的节点集合,因为实际的目的节点我们已经在Send函数中指定了 - 运行时会实例化出多个“worker 任务”,每个 worker 都有自己的 state,进行并发执行
这种并发实际上是由于 LangGraph 支持通过 conditional edges 和 Send 做原生 fan-out/fan-in。我们只需要理解通过这种方式可以实现并行即可。
RevisionLoop 模式
有些任务里,“做出来”不等于“做对了”,所以需要一个 reviewer 专门负责检查、评估和把关。尤其是一些质量标准要求较高的任务,例如 code、写作中,生成 agent 往往会默认自己是对的,而忽略了一些细节上的问题。
RevisionLoop 模式提供了一个 draft -> review -> revise 的小循环,保证生成后能够进行质检,让生成内容更好被验收
from typing import Literal
from langchain.agents import create_agent
from langgraph.graph import END, START, StateGraph
from pydantic import BaseModel, Field
from typing_extensions import TypedDict
from common import get_chat_model
class ReviewVerdict(BaseModel):
status: Literal["revise", "approved"] = Field(description="Whether revision is needed")
feedback: str = Field(description="Reviewer feedback")
class RevisionState(TypedDict, total=False):
topic: str
draft: str
feedback: str
revision_count: int
writer_agent = create_agent(
model=get_chat_model(),
system_prompt="你是 writer agent。根据主题和反馈撰写简洁说明文。",
)
def write_draft(state: RevisionState) -> RevisionState:
feedback = state.get("feedback", "首次生成,请直接输出初稿。")
result = writer_agent.invoke(
{
"messages": [
{
"role": "user",
"content": f"主题:{state['topic']}\n反馈:{feedback}",
}
]
}
)
return {
"draft": result["messages"][-1].content,
"revision_count": state.get("revision_count", 0) + 1,
}
def review_draft(state: RevisionState) -> RevisionState:
reviewer = get_chat_model(temperature=0).with_structured_output(ReviewVerdict)
verdict = reviewer.invoke(
(
"你是 reviewer。若内容没有明确结构、定义不清或过于空泛,则返回 revise。"
f"\n主题:{state['topic']}\n草稿:{state['draft']}\n当前轮次:{state['revision_count']}"
)
)
return {"feedback": verdict.feedback, "status": verdict.status}
def next_step(state: dict) -> str:
if state["status"] == "approved" or state["revision_count"] >= 2:
return END
return "write_draft"
def build_graph():
graph = StateGraph(RevisionState)
graph.add_node("write_draft", write_draft)
graph.add_node("review_draft", review_draft)
graph.add_edge(START, "write_draft")
graph.add_edge("write_draft", "review_draft")
graph.add_conditional_edges("review_draft", next_step)
return graph.compile()
def main() -> None:
app = build_graph()
result = app.invoke({"topic": "为什么多 Agent 不是越多越好", "revision_count": 0})
print("=== Revision Loop Demo ===")
print("Draft:")
print(result["draft"])
print()
print("Feedback:")
print(result["feedback"])
if __name__ == "__main__":
main()
我们使用一部分状态来标记是否需要 revise,这样就能通过一个条件函数判断是否应该停止迭代
这种模式属于典型的 evaluator-optimizer loop 工作流,它比普通的链式调用工作流多了一个闭环和质量门槛。但相应的,因为可能存在的多轮调用,它可能会更慢、更贵。因此只有在任务确实从迭代优化中受益时,再用这种相对更复杂的模式。

浙公网安备 33010602011771号