100 行代码搞懂多 Agent 协同:LangGraph 最小可运行示例(研究员 vs 批评家 + 总结员)

代码已实际验证可跑通,完整代码 95 行(含注释) 默认使用 DeepSeek API,国内友好、便宜,3 分钱跑完一场辩论 适合刚搞懂 LangChain、准备进阶多 Agent 的开发者

一、为什么是这个例子

多 Agent 教程市面上分两种,哪种都不好用:

  • 太玄派:开篇先抽象出 5 个基类、3 个工厂、2 个调度器,看完不知道 Agent 长什么样
  • 太水派:三个 Agent 顺序调一遍 API,起个名叫"协作",其实是个 if-else 也能做的事

这篇要的是第三种 :最少代码 + 真正涵盖多 Agent 的全部核心概念------共享状态、节点函数、条件边、辩论循环、收敛机制。

跑完这 95 行,你就理解了为什么 TradingAgents 那种 75k star 的多 Agent 框架,本质上也是同一套范式的堆叠。

二、先看运行效果

erlang 复制代码
============================================================
辩论历史:
【研究员 R1】AI 不会让大多数程序员失业,而是改变工作内容。
当前 AI 仍需要程序员定义需求、审查代码、处理边缘情况...

【批评家 C1】你的论证忽略了一个关键事实:GitHub Copilot 已经能完成
60% 的样板代码,初级岗位需求在 2025 年明显下降...

【研究员 R2】批评家提到的 Copilot 数据恰好支持我的论点------
样板代码的减少不等于程序员失业,而是把人力解放到更高价值的设计工作...

【批评家 C2】"高价值设计工作"的容量是有限的,不可能容纳所有被替代的初级岗位...
============================================================
最终定稿:
双方共识:AI 显著改变了编程工作的形态。
主要分歧:增量替代 vs 岗位重构...

3 个 Agent、4 次发言、1 次裁决,流程清晰可控。

三、环境准备

arduino 复制代码
pip install langgraph langchain-openai
export DEEPSEEK_API_KEY="你的密钥"   # https://platform.deepseek.com/api_keys

只用 LangGraph + langchain-openai 两个包。DeepSeek 走 OpenAI 兼容协议,所以用 langchain-openai 即可,不需要专门的 DeepSeek SDK。

用其他模型把 base_urlmodel 改一下就行,代码里有标注。

四、核心概念 30 秒速通

LangGraph 多 Agent 的范式就 4 个关键词:

概念 是什么 在代码里长什么样
State 共享状态,所有 Agent 读写同一份"案卷" class DebateState(TypedDict)
Node Agent = 节点函数,签名 state → dict def researcher_node(state)
Edge 节点之间的流转关系 add_edge / add_conditional_edges
Compile + Invoke 编译成可执行图,塞初始 state 跑 graph.invoke(init_state)

整张图长这样:

scss 复制代码
START → 研究员 → 批评家 ─┬─→ 研究员(继续辩论)
                       └─→ 总结员 → END(轮数到了)

批评家之后那个分叉就是条件边------根据 state 里的轮数计数器,决定回去继续辩论还是去总结员收敛。

五、完整代码(95 行,可直接复制)

python 复制代码
"""LangGraph 最小多 Agent 示例:研究员 vs 批评家 + 总结员"""
import os
from typing import TypedDict
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END

# DeepSeek 走 OpenAI 兼容协议,base_url 指向 deepseek 即可
# 换成 OpenAI/Qwen/GLM 只需改 model 和 base_url
llm = ChatOpenAI(
    model="deepseek-chat",
    base_url="https://api.deepseek.com/v1",
    api_key=os.environ["DEEPSEEK_API_KEY"],
    temperature=0.7,
)

MAX_ROUNDS = 2  # 辩论轮数(一轮 = 研究员 + 批评家各发言一次)

# ---------- 1. 共享状态:所有 Agent 读写同一份"案卷" ----------
class DebateState(TypedDict):
    topic: str        # 辩论话题
    research: str     # 研究员最新发言
    critique: str     # 批评家最新发言
    history: str      # 完整辩论历史(累加)
    count: int        # 已进行的回合数
    final: str        # 总结员最终结论

# ---------- 2. 三个 Agent 节点:每个是 state -> dict ----------
def researcher_node(state: DebateState) -> dict:
    prompt = f"""你是研究员,正在辩论:{state['topic']}
你之前的论证:{state['research'] or '(首次发言)'}
批评家上一轮的质疑:{state['critique'] or '(暂无)'}

请用 3 句话提出/强化你的观点,正面回应批评家的质疑(如有)。"""
    resp = llm.invoke(prompt).content
    return {
        "research": resp,
        "history": state["history"] + f"\n【研究员 R{state['count']+1}】{resp}",
    }

def critic_node(state: DebateState) -> dict:
    prompt = f"""你是批评家,正在辩论:{state['topic']}
研究员刚刚的论证:{state['research']}

请用 3 句话指出研究员论证中的具体漏洞或反例,不要泛泛而谈。"""
    resp = llm.invoke(prompt).content
    return {
        "critique": resp,
        "history": state["history"] + f"\n【批评家 C{state['count']+1}】{resp}",
        "count": state["count"] + 1,  # 一回合在批评家这里 +1
    }

def summarizer_node(state: DebateState) -> dict:
    prompt = f"""你是总结员,刚才一场辩论的话题是:{state['topic']}
完整辩论历史:
{state['history']}

请用 5 句话给出最终定稿:1) 双方共识 2) 主要分歧 3) 你更倾向哪一方及理由。"""
    resp = llm.invoke(prompt).content
    return {"final": resp}

# ---------- 3. 路由函数:决定辩论是否继续 ----------
def should_continue(state: DebateState) -> str:
    if state["count"] >= MAX_ROUNDS:
        return "summarizer"   # 轮数够了 → 总结员
    return "researcher"        # 否则回到研究员开下一轮

# ---------- 4. 组装状态图 ----------
workflow = StateGraph(DebateState)
workflow.add_node("researcher", researcher_node)
workflow.add_node("critic", critic_node)
workflow.add_node("summarizer", summarizer_node)

workflow.add_edge(START, "researcher")      # 研究员先发言
workflow.add_edge("researcher", "critic")    # 然后批评家
workflow.add_conditional_edges(              # 批评家之后:循环 or 收敛
    "critic", should_continue,
    {"researcher": "researcher", "summarizer": "summarizer"},
)
workflow.add_edge("summarizer", END)

graph = workflow.compile()

# ---------- 5. 跑一个例子 ----------
if __name__ == "__main__":
    init_state: DebateState = {
        "topic": "AI 会让大多数程序员在 10 年内失业吗?",
        "research": "", "critique": "", "history": "", "count": 0, "final": "",
    }
    result = graph.invoke(init_state)
    print("=" * 60)
    print("辩论历史:")
    print(result["history"])
    print("=" * 60)
    print("最终定稿:")
    print(result["final"])

把代码保存成 debate_demo.py,python debate_demo.py 直接跑。一场完整辩论用 DeepSeek-chat 大约消耗 3-5 千 token,成本不到 3 分钱。

六、5 个关键设计点拆解

代码能跑只是起点,理解为什么这么写才是关键。下面 5 个点是多 Agent 范式的精髓:

6.1 状态用 TypedDict 不用 class

python 复制代码
class DebateState(TypedDict):  # ✅
    topic: str

而不是:

ruby 复制代码
class DebateState:             # ❌
    def __init__(self):
        self.topic = ""

为什么 :LangGraph 的 state 需要可序列化 ------为了支持 checkpoint(跑崩了能从断点续),状态对象必须能存进 SQLite。TypedDict 本质是 dict,天然可序列化;普通 class 还得自己写 __dict__pickle 适配。

这也是为什么 TradingAgents 那么大的项目,几十个字段全部用嵌套 TypedDict 而不是 Pydantic class。

6.2 节点返回 dict 而不是完整 state

python 复制代码
def researcher_node(state) -> dict:
    return {"research": resp, "history": state["history"] + ...}
    # 只返回要更新的字段,不返回整个 state

LangGraph 会自动把返回的 dict merge 进全局 state。这有两个好处:

  1. 节点关注点最小化:每个 Agent 只管自己负责的字段,不用关心其他字段
  2. 并行节点合并不冲突:多个节点并行跑、各自返回不同字段,自动合并无冲突

6.3 辩论历史用"累加"不用"覆盖"

python 复制代码
"history": state["history"] + f"\n【研究员 R{state['count']+1}】{resp}"

这一行是辩论循环里最重要的一行。

  • researchcritique 字段是覆盖式的,只保留最新一轮(给下一个发言者看)
  • history 字段是累加式的,保留全部历史(给总结员看)

两种字段对应两种用途 :Agent 之间快速传递最新观点用覆盖,最终复盘和裁决用累加。这是 TradingAgents 里 Bull/Bear 辩论也用的同一招------它的 InvestDebateState 同时有 current_response(覆盖)和 bull_history / bear_history(累加)。

6.4 count 计数器 + 条件边 = 循环退出

python 复制代码
def should_continue(state) -> str:
    if state["count"] >= MAX_ROUNDS:
        return "summarizer"
    return "researcher"

workflow.add_conditional_edges("critic", should_continue, {...})

LangGraph 没有显式的"while 循环"语法,循环是通过条件边自然形成的:

  • 批评家发言完 → 路由函数判断 → 如果轮数没到,返回 "researcher" → 流转回研究员节点 → 形成循环
  • 轮数到了 → 返回 "summarizer" → 跳出循环

count 在批评家节点里 +1(不在研究员节点),这样保证"研究员 + 批评家"算作一回合而不是半回合。MAX_ROUNDS 这个硬上限是防止 LLM 答非所问导致无限辩论的安全网------生产环境千万别省。

6.5 总结员只读历史 → 给定稿

python 复制代码
def summarizer_node(state):
    prompt = f"完整辩论历史:\n{state['history']}\n请给出最终定稿..."
    # 总结员不参与辩论,只综合

这是收敛的本质:辩论双方负责"把话说透",裁判负责"拍板"。

如果让研究员或批评家自己得结论,他们的立场偏见会污染结果------必须有一个不下场的角色做综合。TradingAgents 里 Research Manager、Risk Manager、Fund Manager 都是这个定位:不参与辩论,只读历史 + 出结论。

这个设计其实是在模仿真实投行的层级------分析师吵,PM 拍板。

七、运行效果实际演示

跑完上面代码,真实输出大致是这样的(节选):

makefile 复制代码
【研究员 R1】AI 不会让大多数程序员失业,理由有三:第一,Copilot 类工具
本质是生产力放大器,历史上每次工具升级都伴随岗位重构而非消失;第二,
软件需求的边际成本下降会刺激新需求出现;第三,创造性工作和系统设计
仍依赖人类判断。

【批评家 C1】第一点用历史类比当下站不住脚------AI 的能力曲线和工业革命
不可同日而语;第二点"新需求"是经济学一厢情愿,目前没有数据支持
软件需求增速能匹配 AI 替代速度;第三点把"系统设计"神化了,
实际上大量"创造性工作"已经被 LLM 拆解为可生成的模板。

【研究员 R2】批评家的反驳恰好暴露了一个隐含假设:把"程序员"等同于
"代码搬运工"。Stack Overflow 2024 调研显示,资深程序员花在写代码的
时间不到 40%,其余是沟通、设计、决策...

【批评家 C2】...

============================================================
最终定稿:
双方共识:AI 显著改变了编程工作的形态,初级岗位首当其冲。
主要分歧:研究员认为这是渐进式重构,批评家认为是结构性替代。
我更倾向研究员的论证,因为他引用了具体数据,而批评家更多依赖类比推理。
但研究员需要补充一个关键证据:被解放的人力能否匹配新岗位的技能要求。

注意第三轮研究员针对性地反驳了批评家第一轮的具体观点------这就是 state 流转的效果,Agent 之间是有真实信息传递的,不是各说各话。

八、扩展方向

这 95 行是起点,LangGraph 的能力远不止此。几个常见扩展方向:

想加的能力 怎么做
并行多个研究员 多个节点 fan-out,LangGraph 自动并行执行,merge 进 state
给 Agent 工具 llm.bind_tools([...]) + ToolNode,Agent 能自己搜资料、查 API
断点恢复 MemorySaver()SqliteSaver() 一行加进 compile,跑崩了能续
人在回路审批 interrupt() API,在裁判前暂停等人确认
替换模型 ChatOpenAI(model="...", base_url="..."),Claude / GPT / Qwen / Ollama 一键切
可视化图结构 graph.get_graph().draw_mermaid_png() 直接出 mermaid 图
流式输出 graph.stream(init_state) 替代 invoke,逐节点 yield 中间结果

九、总结

多 Agent 不神秘,核心就三件事:

scss 复制代码
节点(Agent 函数) + 状态(共享案卷) + 图(协同编排)

记住下面这张速查表,你就掌握了 LangGraph 多 Agent 开发的 80%:

想做 用什么
定义状态 class XxxState(TypedDict)
定义 Agent def xxx_node(state) -> dict
顺序流转 workflow.add_edge(A, B)
分支 / 循环 workflow.add_conditional_edges(...)
编译 graph = workflow.compile()
执行 graph.invoke(init_state)
流式 graph.stream(init_state)
断点恢复 compile(checkpointer=MemorySaver())

剩下 20% 是 checkpoint、人在回路、并行 fan-out 这些进阶技巧,等需要的时候再学不迟。

完整代码已经放在文章第五节,复制即可跑通。可以改成自己需要的角色配置------把"研究员 vs 批评家"换成"程序员 vs Code Reviewer"、"产品 vs 用户"、"乐观派 vs 悲观派"都是同样的代码框架,只改 prompt 就行。

相关推荐
小李子呢02112 小时前
什么是agent?
agent
带刺的坐椅3 小时前
Spring AI 2.0 GA 倒计时:先别急,来看看 Java AI 框架的另一条路
java·spring·ai·llm·agent·solon
JunLa3 小时前
Agent Basic 上篇
大数据·人工智能·agent
janeysj3 小时前
OpenDeepResearch源码解析和二次开发
人工智能·langchain
爱跑步的程序员~5 小时前
RAG 技术全面解析:从原理到实践
python·ai·langchain·rag
Fleshy数模6 小时前
玩转 LangChain:从 Prompt 模板到多场景 AI 交互实战
人工智能·langchain·llm
嘻嘻仙人6 小时前
从原理到代码,拆解AutoGen框架开发实践
人工智能·agent
xuco6 小时前
如何使用 Semantic Router 减少 Token 使用量
人工智能·agent
夜影风6 小时前
LangGraph实战:搭建一个带人工介入的智能客服系统
人工智能·langchain·langgraph