langgraph多智能体协作设计

概述

个人项目:https://github.com/a-gua-a/frog_ai_py

我的langgraph编排逻辑如下:
plan节点做任务规划,按照react模式将任务拆成多端,但是拆分时需要注意一点,要将可以并行、不相干的任务放在同一轮并行实现,每一轮规划时需要格式化输出当轮需要并行的taskList。

distribute(利用send实现的特殊条件边):在信息完全且用户意图为工作命令时,使用send将任务分发至work节点;在用户意图为闲聊时,直接发至report节点;当用户意图为工作命令且命令信息不全时,指向interruptNode节点。

interruptNode节点:根据上一节点的询问需求,使用interrupt()发起中断,获取用户回答,随后回到发起询问的节点,该节点中使用resume续传信息。

work节点:根据send分发的任务调用工具,并行实现。

多个并行的work组成的超级步骤节点通过普通边指向check节点。

check节点负责检查当前任务是否正确完成,若有问题提出反馈,如果所有任务完全完成且通过检验,则给出结束标志。

isPss条件边:根据结束标志路由,已结束则指向report节点,未结束就给出retry标记指向plan节点。

再次回到plan节点时,plan节点发现retry标记,使用另外的提示词(react内提示词)继续新一轮react。

plan、work、check、report节点调用智能体完成工作,引导智能体生成包含所需的所有字段的描述,然后通过outlines进行一次普通的大模型调用,将前面智能体生成的描述字段总结成需要的结构化数据,将结构化数据计入state,保障节点间的数据交流。

react:先思考,再行动,然后根据结果调整

示例:

查询明天北京到上海的高铁并预订一等座”

思考:“我需要先查询车次信息”->行动 :模型调用相应工具,如“调用12306车次查询API”->观察:模型接收API返回的车次列表

    ->迭代:模型根据车次列表再次进入“思考”阶段,决定下一步是“预订一等座车票”,并再次执行“行动”和“观察”

    ->完成 :当所有子任务完成后,模型整合信息,向用户输出最终答案,如“已成功预订XX车次一等座”

关于项目:

在plan节点做好提前规划,将任务拆分成多端

map-reducer & Send 分发:并行处理、 “分而治之” 

 在条件边中使用send()分发:

Send 是 LangGraph 中用于实现动态并行(Dynamic Parallelism)的核心工具。它允许你在图运行时,动态地将一个任务“拆解”成多个子任务,并让它们并行执行。
Send 必须通过条件边(Conditional Edge) 返回一个 Send 对象列表。当图执行到这条条件边时,调度器会并行地执行列表中指定的所有节点任务。

async def distribute(state : GlobalState, config: RunnableConfig):

    if not state.workFlowNeed:
        return "report"

    if state.isInterrupt:
        return "interruptNode"

    logger.info("开始分发任务")

    return [
        Send(
            "work",
            DistributedState(
                task=task,
                threadId=config.get("configurable", {}).get("thread_id", ""),
                runId=state.runId
            )
        )
        for task in state.taskList
    ]

适用场景:生成关于多个主题的笑话,通过plan将命令拆分成多条任务,每条任务对应一个主题的笑话生成。

toolNode

ToolNode 是 LangGraph 提供的一个预构建节点(Prebuilt Node),它的核心职责是可靠、高效地执行 LLM 请求调用的工具

ToolNode 的工作流程非常清晰:
接收状态:它接收图的当前状态(State),并默认从中提取 messages 列表。
解析调用:它会检查 messages 列表中的最后一条消息,看其是否为一个包含 tool_calls 的 AIMessage。
并行执行:如果存在多个工具调用,ToolNode 会并行执行它们,以提高效率。
返回结果:每个工具调用的结果会被封装成一个 ToolMessage 对象。这些 ToolMessage 会被添加回 messages 列表中,作为状态的一部分进行更新。

使用示例:

tools = [get_weather, get_current_time]
tool_node = ToolNode(tools)
builder = StateGraph(MessagesState)
builder.add_node("chatbot", chatbot)
builder.add_node("tools", tool_node)

Fuction Calling(Tool Calling)

简要描述:先告诉大模型可以使用的工具描述(实现的功能、入参、结果等)->在需要调用工具时,大模型会回答一个toolMessage,包含使用什么工具、入参如何->程序按照大模型回答的toolMessage调用工具(提前编写好的函数)->程序再将调用函数获取到的结果回复给大模型(当然,也必须依赖上下文实现记忆)->大模型根据收到的回复继续思考、答复。

核心是特殊Token(Special Tokens) 与系统提示(System Prompt) 的协同工作。这项能力并非模型“天生”就有的,而是通过专门的训练习得的。特殊Token是模型词汇表中一组被特意保留的、用于特定目的的Token。当模型决定调用工具时,它会在生成的文本中输出这些特殊的Token序列,作为“我要开始调用工具了”的明确信号。

image

 MCP(模型上下文协议,Model Context Protocol)

简述:为AI应用与外部数据源、工具和服务之间的连接,提供一个统一、标准化的方式。依照JSON-RPC 2.0 协议规范,SSE或http/https传输。

每个AI模型(如GPT、Claude)都有自己独特的工具调用方式,开发者需要为每个模型编写不同的适配代码,工具的具体实现与AI模型的代码深度绑定,难以复用和扩展,在多轮对话或复杂任务中,管理状态和传递上下文信息非常困难。MCP通过提供一个与模型无关的标准化协议,让任意AI模型都能通过统一接口调用任意工具,从而解决了上述问题。

一个MCP服务器主要对外提供三种核心能力:
资源 (Resources):类似于文件系统的“读”操作,为AI提供可读取的上下文数据,如本地文件、数据库记录等。
工具 (Tools):类似于函数调用,是AI可以执行的“写”操作或动作,如发送邮件、查询天气、运行命令等。
提示词 (Prompts):预定义的模板化对话流程,可以帮助用户更规范地与AI交互。

Agent Skill

可以理解为AI的 “入职手册”与“工具箱” 。它是一个标准化的能力包,将专业知识、工作流程和最佳实践封装起来,让通用的AI大模型能像经验丰富的老员工一样,专业、稳定、高效地完成特定任务。

skill在一定程度上就是特殊的prompt。

一个标准的Agent Skill本质上是一个文件夹,其核心是SKILL.md文件,通常还包含其他资源:
SKILL.md (技能说明书):
YAML元数据:文件开头的---部分,必须包含name(名称)和description(描述)。description至关重要,是Agent用来匹配任务的“索引”。
Markdown指令:详细的核心指令、工作流程、输入输出规范、示例等。
scripts/ (脚本目录):存放可执行的辅助脚本(如Python、Bash),用于完成具体操作。
references/ (参考资料):存放详细的参考文档、业务规则等,按需加载。
assets/ (资源文件):存放模板、图片等静态资源。

Skill的一个精妙设计在于渐进式披露(Progressive Disclosure),它解决了在不撑爆模型上下文窗口的前提下,赋予其大量专业知识的问题。
这个机制分三层,按需加载:
第一层:元数据(轻量级索引):Agent启动时,只加载所有已安装Skill的名称和简短描述。这就像一份目录,非常轻量,装成百上千个Skill也毫无压力。
第二层:核心指令(按需加载):当用户提出需求,Agent会将其与所有Skill的简短描述进行匹配。一旦匹配成功,才会完整加载对应SKILL.md文件中的核心指令和工作流程。
第三层:外部资源(执行时查阅):在执行具体步骤时,如果需要,Agent才会去读取references/(参考文档)或执行scripts/(脚本文件)。
这种设计既高效(省Token)又精准(聚焦当前任务)

MCP vs Function Calling

Function Calling 更像是一种接口规范,定义了AI模型“如何”在请求中描述一个工具调用。
MCP 则是一个完整的通信协议,它定义了AI应用与工具之间“如何”建立连接、发现能力、传递上下文和执行调用的全过程。
可以这样理解:
Function Calling 是AI模型用来“说话”的语法。
MCP 则是连接AI与外部世界的神经系统和统一接口。
MCP的出现,正是为了解决不同AI模型使用不同“语法”(Function Calling)所带来的碎片化问题

 

MCP vs Function Calling vs Skill

Function Calling(函数调用)—— 厨师的具体“动作”
角色:这是厨师的基本功,是执行具体操作的单一动作。
类比:比如“切菜”、“颠勺”、“开火”、“加盐”。这是完成烹饪最基础、最原子的单位。
特点:
原子性:每个动作只做一件事。
无状态:厨师不需要知道这道菜是为谁做的,只需要执行“切黄瓜丁”这个指令。
直接执行:它不负责思考“该不该切”,只负责“怎么切”。
在AI中的体现:get_weather(location) 或 send_email(to, subject)。它只负责执行一个具体的函数,并返回结果。

 

MCP(模型上下文协议)—— 标准化的“厨具接口”
角色:这是后厨的标准化厨具和统一的水电气接口。它解决了“怎么连接”的问题。
类比:比如一个标准的“燃气灶接口”。无论是中式的猛火灶,还是西式的平底炉,只要接口标准统一,都可以即插即用。同样,一个标准的“USB-C充电口”,可以给手机、平板、笔记本充电。
特点:
标准化:定义了统一的通信协议(JSON-RPC),任何遵循此协议的工具都能被连接。
解耦:AI系统不需要关心工具内部是Python写的还是Java写的,只要它暴露了MCP接口。
连接层:它不处理业务逻辑,只负责“打通道路”。
在AI中的体现:一个MCP服务器把get_weather和send_email封装成标准工具。任何支持MCP的AI客户端(如Claude Desktop)都能直接“插上”这个服务器,而无需为每个API单独写适配代码。

 

Skill(智能体技能)—— 标准化的“菜谱和操作流程”
角色:这是资深大厨的“独门秘籍”和“标准作业程序(SOP)”。它解决了“该怎么做”的问题。
类比:一份“宫保鸡丁标准菜谱”。它不只告诉你要“切菜”和“加盐”,而是定义了:
步骤:先腌肉,再调汁,最后爆炒。
标准:花生米要炸到“八分熟”,辣椒要“炒出红油”。
工具:使用“炒锅”而非“汤锅”(对应调用合适的工具)。
特点:
流程化:它是一个多步骤的完整流程,而不仅仅是单一动作。
封装知识:包含了特定领域的专业知识(如何让鸡丁嫩滑)和业务规则。
按需加载:平时只需要知道有“宫保鸡丁”这道菜(元数据),只有顾客点了,大厨才拿出详细的菜谱(SKILL.md)来执行。
在AI中的体现:一个“代码审查Skill”会定义:1. 先检查代码风格;2. 再运行静态分析;3. 最后生成审查报告。每一步都可能调用不同的MCP工具(如连接Git的MCP、连接静态分析工具的MCP)。

 RAG

RAG(检索增强生成,Retrieval-Augmented Generation) 是一种将信息检索系统与大语言模型相结合的技术架构。它的核心理念是:让LLM在回答问题时,先从外部知识库中“查找”相关信息,再基于这些信息进行“生成”,从而弥补大模型知识陈旧、容易“幻觉”的缺陷。

RAG生效过程:

数据加载:将原始文档(PDF、网页、数据库等)导入系统。
数据分块(Chunking):将长文档切分成较小的、语义完整的文本块(Chunk)。例如,一篇50页的报告可能被切成数百个段落。
向量化(Embedding):使用一个嵌入模型(Embedding Model),将每个文本块转换成一组固定长度的、代表其语义的浮点数数组,即向量(Vector)。
存储至向量数据库:将生成的向量及其对应的原始文本,存入专门为高效相似度搜索而设计的向量数据库(Vector Database) 中。
接收用户提问:用户输入一个问题。
向量化查询:使用同一个嵌入模型,将用户的问题也转换成向量。
相似度检索:去向量数据库中,查找与问题向量在“语义”上最相似的K个向量(K通常为3-10),并取出这些向量对应的原始文本块。
构建增强提示(Augmented Prompt):将检索到的文本块作为“参考资料(Context)”与用户的原始问题一起,按照一个精心设计的Prompt模板,组合成一个新的、完整的提示词。例如:
LLM生成答案:将构建好的增强提示发送给大语言模型,模型基于提供的资料生成最终答案。

 

关键组件
嵌入模型(Embedding Model):负责将文本“翻译”成向量。它决定了语义检索的精度。常用的有 text-embedding-ada-002、BGE 等。
向量数据库(Vector Database):负责存储和检索向量。它必须支持高效的近似最近邻(ANN)搜索。常用的有 Pinecone、Milvus、Qdrant、Weaviate,以及基于 pgvector 插件的 PostgreSQL。
分块策略(Chunking Strategy):切分块的大小(如512 tokens)和方式(按段落、按句子、滑动窗口)直接影响检索质量。太小可能语义不完整,太大可能包含噪声信息。

 

image

 向量归一化:在不改变向量“方向”的前提下,将向量的“长度”(模长)缩放到一个统一的标准,通常缩放为 1,也就是变成所谓的单位向量。

“RAG更关心两段文本‘在说同一件事’,而不是‘说话的音量有多大’。”
余弦相似度反映的是“方向一致性”:它的本质是测量两个向量在高维空间中的夹角。夹角越小,余弦值越接近1,意味着它们在语义上“指”向同一个方向。
相比之下,其他度量方式各有缺陷:
欧几里得距离:它关注的是两个向量端点之间的绝对直线距离。但这会受到向量长度(模长)的干扰。比如,“巨大”和“庞大”这两个词,语义相近,如果向量方向一致但长度不同,欧氏距离可能会很大,导致错误判断。
简单点积:点积计算(q · d)的结果受向量长度影响很大,一个向量很长可能会让点积结果虚高,不利于公平比较。
其次,是工程实践上的考虑。
归一化后的高效计算:在实际的生产级RAG系统中,我们会先对所有向量执行 L2归一化(把每个向量的模长都变成1)。在这一步之后,余弦相似度的计算就神奇地等价于计算点积(q · d)。点积在现代CPU/GPU上计算速度极快,可以轻松支持对亿级向量的实时检索。

面对未归一化的向量,首先评估是否能进行离线重索引(归一化更新),这是最好的办法。如果受限于条件无法修改,在检索时显式计算带分母的完整余弦相似度,同时为每个向量额外存储一个模长字段以备计算。核心原则是:绝对不能为了性能,在未归一化的数据上直接使用点积来计算余弦相似度,那是严重的数学错误。

prompt管理

基于 langgraph_kit 的 PromptAssembly 框架构建了一套高度模块化、动态可组合的智能提示系统,其核心优势在于将传统静态提示词拆解为角色定位、工作规则和上下文注入三个层次,通过 PromptSection 和 SectionStability 机制实现了提示词的智能生命周期管理——STABLE 稳定段(如角色定义和核心工作流)在对话全程保持不变,避免重复注入造成的 token 浪费,而 VOLATILE 易变段(如聊天模式切换提示)则根据用户意图动态加载,这种分层稳定性策略大幅降低了上下文窗口的冗余占用;更关键的是,项目创新性地引入了 ContextProvider 动态上下文注入机制,通过 PlanAgentRetryProvider、CheckAgentQueryProvider、ChatAgentQueryProvider 等专用提供者类,在运行时根据任务状态、执行结果和检查反馈实时生成针对性提示,例如当任务未通过检查时,RetryProvider 会自动将原始需求、历史任务列表、已完成工作和检查反馈组装成结构化的重试指令,引导规划专家进行增量式修正而非全量重新规划,这种动态上下文注入不仅使提示词具备了状态感知能力,还实现了提示内容与执行流程的深度耦合;此外,项目采用 PromptManager 统一管理层和 PromptComposer 组合器,支持单个或批量注册提示片段,并在 Agent 初始化时通过 registerPrompt 方法按需装配,配合结构化输出约束(StructLimitProvider 和 StructProcessProvider)确保 LLM 输出严格符合预定义的 Pydantic 模型,整个提示词体系呈现出高内聚低耦合的特征——每个 Agent 拥有独立的角色定位和工作规则,通过 taskId 实现任务隔离,同时共享通用的上下文提供者,既保证了专业分工的明确性,又维持了系统整体的协调一致,最终形成了一个既能适应多 Agent 协作复杂场景,又能根据运行时状态灵活调整提示策略的智能化提示工程架构。

提示词工程细节(提示词缓存,节省token)

langgraph_kit 的 PromptAssembly 框架节省 Token 的核心原理,是利用并最大化 Anthropic 等大模型的“提示缓存(Prompt Caching)”能力。
简单来说,它通过智能地重组提示词(Prompt),将频繁变动的部分与长期稳定的部分分离,让模型能够复用已计算过的内容,从而避免重复计费和计算。

核心机制:稳定优先(Stable-First)排序
这是 PromptAssembly 最核心的策略。它将系统提示词(System Prompt)的各个部分按照“稳定性”划分为三种类型:
STABLE (稳定):极少变化的内容,如代理的核心身份、固定指令、通用工具说明等。
VOLATILE (易变):每次对话轮次都可能变化的内容,如当前会话的上下文、用户问题、最新的记忆摘要等。
CONDITIONAL (条件):仅在特定条件下才激活的内容,如当需要处理代码时,才加入编码工作流的指令。
PromptAssembly 在组装提示词时,会强制将所有的 STABLE 部分排在提示词的最前面,然后将 VOLATILE 和 CONDITIONAL 部分依次放在后面。

Anthropic 等大模型的“提示缓存(Prompt Caching)”能力

提示缓存(Prompt Caching)是一项通过复用之前已处理过的提示词(Prompt)内容,来显著降低 API 使用成本和延迟的技术。它的核心思想是:既然很多请求都包含相同的系统指令、工具定义或长上下文,为什么不让模型“记住”它们,而每次都重新处理一遍呢?

提示缓存的实现依赖一个关键原则:缓存是基于提示词的前缀(Prefix)进行精确匹配的。
缓存键的生成:当你的请求启用了缓存,Anthropic 会使用从提示词开头到指定“缓存断点(Cache Breakpoint)”的内容,生成一个加密哈希值作为缓存键(Cache Key)。这个键可以理解为这段提示内容的“唯一指纹”。
命中与未命中:
缓存命中(Cache Hit):如果后续请求的提示词前缀与这个“指纹”完全一致,系统就会直接复用缓存的键值(KV)状态。
缓存未命中(Cache Miss):如果前缀有任何微小的变化——哪怕只是一个空格或大小写的不同——系统都无法命中缓存,必须全量处理。
缓存的内容:缓存的是提示词中直到并包括最后一个用 cache_control 标记的块的所有内容。这个前缀包含了 system、messages 和 tools,并按此顺序组织。
注意:缓存命中的前提是两次请求的前缀完全一致。因此,任何动态变化的内容(如时间戳、用户ID)如果放在系统提示词的开头,都会破坏缓存。

提示缓存在成本和速度上的优势非常突出:
成本效益:使用缓存内容的成本仅为标准输入价格的 10%(即享受 90% 的折扣)。而首次创建缓存(Cache Write)的成本会比标准输入高 25%。
性能提升:对于长提示词,首字生成时间(TTFT)的延迟最多可降低 85%。例如,处理一个10万Token的书籍,响应时间可从11.5秒降至2.4秒。

Anthropic 提供了两种启用缓存的方式:
自动缓存(Automatic Caching):在请求顶层添加一个 cache_control 字段即可。系统会自动将断点放在最后一个可缓存块,适合多轮对话场景。
显式缓存断点(Explicit Cache Breakpoints):在 system、messages 或 tools 等具体内容块上精细控制缓存位置。这适合需要对缓存内容进行精细控制的场景。

缓存不是永久有效的,它有一个生存时间(TTL)。
默认 TTL:5 分钟。每次缓存被命中,其 TTL 都会自动刷新,且不产生额外费用。
延长 TTL:若 5 分钟不够,可付费选择 1 小时的缓存时长。

“付费”是指为创建这个1小时缓存的操作支付更高的单次写入费用。你可以通过 API 请求中的一个参数来启用它,费用会直接体现在你的 API 账单上。

image

为了充分利用提示缓存,可以遵循以下实践:
稳定优先排序:将最稳定、最不可能变化的内容(如系统角色、核心指令、工具定义)放在提示词的最前面。确保这部分内容形成一个固定的、可预测的缓存前缀。
分离动态内容:将动态变化的内容(如用户问题、会话ID、当前时间)放在提示词的末尾。这样就不会影响前面稳定部分的缓存。
保持前缀一致:确保所有请求的前缀部分完全一致,哪怕多一个空格或换行都会导致缓存失效。
权衡首次创建成本:缓存写入成本比标准输入高 25%,对于仅使用一次的提示词,启用缓存可能不划算。
选择合适的 TTL:根据业务场景选择合适的 TTL。如果两次请求间隔经常超过 5 分钟,考虑使用 1 小时 TTL。
利用框架支持:使用 LangChain 等框架的中间件(如 AnthropicPromptCachingMiddleware)可自动管理 cache_control 标记。

上下文污染的产生与解决

项目中 taskId 的核心目的是在多 Agent 协作的复杂工作流中实现严格的上下文隔离与任务边界划分,它作为每个任务的唯一标识符贯穿规划、执行、检查和报告的全生命周期,通过 GlobalState 和 DistributedState 在节点间传递,配合 TaskDiffProvider 动态注入当前任务标识到各个 Agent 的提示词中,使 WorkAgent 和 CheckAgent 能够严格遵循"仅关注当前 taskId 的任务、忽略其他 taskId 信息"的隔离规则,从而确保每个 Agent 在处理并行或串行任务时不会被历史任务的残留信息干扰;一旦失去 taskId 机制,上下文污染将呈现灾难性的连锁反应——首先,WorkAgent 在执行新任务时会混淆历史任务的描述和目标,导致执行方向偏离甚至完全错误,例如原本要求"查询张三的地址"的任务可能被之前"分析销售数据"的任务结果污染,使 Agent 输出混杂两种主题的无效内容;其次,CheckAgent 在评估完成度时无法区分哪些结果属于当前任务、哪些属于过往任务,可能将已完成的历史任务误判为当前任务的成果,或者遗漏当前任务的实际完成情况,造成质量检查的严重失真;更严重的是,PlanAgent 在重试规划时会失去增量修正的能力,无法识别哪些任务已完成、哪些需要补充,导致全量重新规划而非针对性修正,不仅浪费 token 和计算资源,还会使任务列表出现大量重复或冲突的条目;最终,ChatAgent 在生成总结报告时会将所有历史工作结果混为一谈,无法按任务维度组织信息,输出的报告将变成杂乱无章的信息堆砌,完全丧失专业性和可读性,整个多 Agent 协作系统将从精密的任务流水线退化为混乱的信息漩涡,每个 Agent 都在被污染的全局上下文中做出错误决策,形成恶性循环。

节点间交流(Outlines结构化限制)

采用 Pydantic 强类型实体(PlanResult、Task、WorkResult、CheckResult 等)作为节点间通信的唯一数据载体,先引导智能体描述完整信息,再使用 Outlines 库在 LLM 推理时通过 token 级 logit masking 强制生成符合 JSON Schema 的输出,再由 structure() 函数解析为强类型对象;同时,每个节点的系统提示词中通过 StructLimitProvider 动态注入目标结构的 JSON Schema,让 LLM 在生成时"看到"期望格式,而 GlobalState 中 results 字段使用 operator.add reducer 实现多 work 节点并行执行后的结果自动合并,最终形成一条从 Plan → distribute → Work(并行)→ Check → Report 的类型安全、格式严格、自动合并的全链路结构化数据流,从根本上杜绝了多 Agent 协作中常见的"幻觉格式"和"字段漂移"问题。

与其他限制方式对比:

image

 

LangGraph 的 stream() / astream() 方法  多种流式输出模式

"values" — 全量状态快照

此模式在每个超级步骤(Super-step)完成后,会流式传输整个图的状态(State)。它适合用于前端需要刷新完整页面或展示最新全局状态的场景,例如在对话应用中更新完整的聊天历史记录。使用时需要注意,如果图的状态对象很大,频繁传输完整状态可能会带来较大的网络开销。

"updates" — 增量状态更新

此模式仅返回单个节点执行完成后产生的状态变更(Delta),而非完整状态。它非常适合用于追踪任务执行进度,比如监控一个多节点工作流中“哪个节点刚刚完成”、“输出了什么结果”。与 "values" 相比,"updates" 传输的数据量更小,更利于细粒度的进度展示。

"messages" — LLM Token 流式输出

此模式专为大语言模型(LLM)调用场景设计,它会在 LLM 生成回复时,逐 Token 或按块流式地输出消息内容。产出的数据是 (消息块, 元数据) 的元组形式,其中元数据包含了模型名称、节点ID等信息。这是实现类似 ChatGPT 打字机效果(逐字显示)的核心模式,能显著提升终端用户的交互体验。

"custom" — 自定义数据流

此模式允许图节点在内部通过 StreamWriter 对象发送完全自定义的数据。开发者可以在节点执行的任意位置注入自定义消息(例如:“正在检索知识库...”、“正在分析问题...”),用于向客户端反馈复杂的中间执行状态或非标准数据,极大地增强了应用的交互灵活性和透明度。

"debug" — 详细调试信息

此模式会流式传输图执行过程中产生的详尽调试事件,包括节点的开始/结束、状态变更的每一步细节等。它主要用于开发阶段的深度排查,帮助开发者精确追踪图的执行流程和状态演化路径。由于信息量极大且偏向底层,通常不建议在生产环境中开启。

"checkpoints" — 状态检查点

此模式用于流式传输图执行过程中的状态快照(检查点)。这些检查点可以用于实现应用的持久化存储、状态回滚或审计日志等功能。通过订阅该模式,开发者可以捕获图在特定时刻的完整状态,为构建健壮、可恢复的应用程序提供底层支持。

"tasks" — 任务事件监控

此模式流式传输的是图执行过程中任务(Task)的生命周期事件,例如任务的创建、调度、执行开始、执行完成等。它为开发者提供了对 LangGraph 底层任务调度机制的可见性,适合用于构建高级的监控仪表盘或实现复杂的自定义调度逻辑。

多智能体合作的优势、难点

优势:

借鉴人类社会的分工逻辑,每个智能体可以专注于特定领域(如编程、数学、检索或安全审查),并加载专属的提示词、知识库或工具集。这避免了让一个通用大模型去处理所有任务,从而在各自领域达到更高的精度和深度。
对于可以分解的复杂任务(如同时分析多份不同结构的文档),多智能体可以并行执行,显著缩短总处理时间,提升系统吞吐量。
系统不再依赖单一模型。当某个智能体出错时,其他智能体(如“监督员”或“评审员”)可以通过交叉验证、投票或反馈机制进行纠错,使得系统整体表现更加稳定,具备“自我修正”的潜力。
让不同角色(如“激进的产品经理”与“保守的法务”)进行多轮辩论,往往能促使模型进行更深度的思考,避免盲目从众和思维固化,产生出单一模型难以涌现的优质解决方案。】

难点:

如何设计有效的协商机制是一大难题。智能体之间可能因目标冲突(如“降本” vs “增效”)而陷入无休止的争论,或者因无限循环引用(A依赖B,B依赖A)导致流程死锁。如何仲裁分歧、达成共识,缺乏通用的成熟算法。
当最终结果不尽如人意时,很难准确追溯是哪个环节(哪个智能体)出了问题。这种“信用分配”难题导致开发者难以针对性地优化某个子智能体,也使得基于最终奖励进行全局反向传播变得极为困难。
智能体间频繁的通信会消耗大量Token,产生高昂的API调用成本和显著的延迟。同时,随着对话轮次增加,长距离的历史交互信息可能在上下文中被稀释,导致后续智能体忽视早期的关键决议,产生“遗忘”现象。
多个智能体的交互天然具备“涌现性”,使得系统行为变得难以预测。相比单个模型的“黑盒”,多智能体系统是“多个黑盒的耦合”,调试难度呈指数级上升,且系统表现方差极大,难以进行稳定的单元测试。
交互空间的扩大引入了新的攻击面。恶意指令可能通过“欺骗”某个较弱的智能体,利用其作为“代理”来操纵整个系统,或者智能体之间可能产生人类意料之外的“密谋”协作方式,带来潜在的安全失控风险。

个人项目设计:

本项目采用 Plan → Work → Check → Report 四阶段多智能体协作模式,其核心优势在于职责分离与闭环校验:LeaderPlanAgent 专注意图分析与任务拆解,将复杂需求转化为结构化的 Task 列表,并在信息不全时通过 interrupt 机制主动向用户询问补充,避免"猜测式执行";LeeWorkAgent 作为纯执行者,按 DistributedState 接收独立任务,通过 LangGraph 的 Send 机制实现多任务并行执行,各 work 节点互不干扰、结果通过 operator.add reducer 自动合并;LivCheckAgent 充当质检关卡,对照原始需求与执行结果进行客观评估,不通过则回退到 Plan 节点重新规划,形成"执行→检查→重试"的自我纠错闭环;LuciaChatAgent 负责最终汇报或简单闲聊,使用通用模型而非结构化模型,保证回复的自然流畅。这种**"规划者-执行者-检查者-汇报者"**的角色分工,配合 Pydantic 强类型实体 + Outlines 结构化约束,使得每个 Agent 只需关注自己的单一职责,既降低了单个 Agent 的提示词复杂度和出错概率,又通过类型安全的节点间通信确保了全链路数据的一致性和可追溯性。

智能体直接作为节点与节点内调用智能体的区别

智能体直接作为节点
优点:
清晰解耦:将复杂任务拆分为由不同专家智能体负责的模块,逻辑清晰,便于不同团队独立开发和维护。
专注高效:每个智能体可专注于特定领域,指令集更窄,能将上下文集中于子任务,精度更高。
易于复用:一个封装好的智能体节点,如同一个标准化的“技能”,可被多个上层工作流调用。
缺点:
资源消耗高:每个智能体的启停、推理都会消耗额外的资源和Token,导致成本增加。
延迟增加:特别是在串行执行时,多个智能体的顺序调用会累加延迟。
协调复杂:需要设计额外的机制(如监督者智能体)来协调多个节点间的任务分配和结果整合。
适用场景:
需要多个高度专业化的智能体协作完成复杂任务(如金融分析、多市场调研)。
构建可插拔、可复用的“技能库”,供不同上层应用调用。
任务本身具有清晰的模块化结构,可以自然拆解。

节点内调用智能体
优点:
实现简单:在现有节点逻辑中直接集成,开发和部署路径更短。
控制精准:父节点可以精确控制子智能体的调用时机和输入,流程更可控。
上下文隔离:子智能体不感知父节点冗长的对话历史,只需专注于当前明确的任务,减少干扰。
缺点:
耦合度高:子智能体与父节点的逻辑绑定紧密,修改或替换成本高,不利于独立演进。
复用性差:作为特定节点的一部分,难以被工作流的其他部分或其他项目复用。
调试困难:当问题发生在嵌套调用深处时,排查和定位问题的难度会增加。
适用场景:
在已有的工作流节点中,需要临时增强一个“智能”能力(如在对话中按需查询订单)。
作为一个轻量级工具,供主智能体在特定条件下调用。
对成本和响应速度要求较高,希望通过精确控制来优化资源使用。

个人项目选择:

本项目选择"节点内调用智能体"而非"智能体直接作为节点",根本原因在于项目的核心价值——结构化类型安全的节点间通信——必须依赖节点函数作为编排适配层,而无法由 Agent 自身承载。

具体而言,有四重不可替代的优势:第一,运行时提示词组装——每个节点在执行前需要根据当前 GlobalState 动态注入目标结构的 JSON Schema(StructLimitProvider)、任务标识(TaskDiffProvider)、重试上下文(PlanAgentRetryProvider)等,这些信息是运行时才能确定的,若 Agent 直接作为节点,其 system_prompt 在初始化时已固化,无法接收运行时的动态参数;第二,输出后处理防线——每个节点在 Agent 输出后必须经过 StreamWriter 流式事件捕获和 structure() 二次解析(Ollama + Outlines 强制结构化),才能将自然语言文本安全地转化为 Pydantic 强类型对象并更新 GlobalState,若 Agent 直接作为节点,LangGraph 只能拿到 Agent 的原始返回值,这道"类型安全闸门"无处安放;第三,控制流与执行流分离——distribute、isPass、interruptNode 等纯控制流逻辑与 plan、work、check、report 等纯执行流逻辑在图中各自独立,修改路由规则不影响 Agent 提示词,反之亦然,若 Agent 直接作为节点,条件路由和重试机制必须通过额外的监督者 Agent 来实现,导致图结构复杂化且引入不必要的 LLM 推理开销;第四,四层防护从"物理不给"(DistributedState)→"存储不通"(独立 checkpointer)→"输入不传"(白盒构建 userInput)→"提示词不认"(隔离规则),层层递进,确保每个 Agent 只能看到它需要看到的信息,不会受到其他 Agent 或历史任务的上下文污染。

正是这四重需求——运行时动态提示词注入、输出后处理类型安全、控制流与执行流解耦、避免Agent间上下文污染——构成了"智能体直接作为节点"无法成为更优解的原因:Agent 作为 LangGraph 图中的节点时,它只是一个黑盒执行单元,既无法在调用前根据上游状态动态调整自身行为,也无法在调用后对输出做二次加工再写入下游状态,这与本项目对节点间通信"强类型、可验证、可追溯"的刚性约束存在根本性冲突。

智能体直接作为节点的输入来源(父子的键共享)

当父图和子图的状态模式(State Schema)中定义了完全相同的一个或多个状态键时,它们就可以通过这些“共享通道”进行通信。
工作原理:子图(智能体)可以直接读取父图State中共享键的值,也能修改这些键的值,从而实现数据的双向传递。
如何实现:在LangGraph中,这通常通过将编译好的子图(subgraph)直接作为节点添加到父图中来完成。
适用场景:适用于父子图需要频繁、紧密协作的场景,例如多智能体系统通过共享的messages键来传递对话历史。

父子记忆(state管理)的关系及各自的重要性(基于个人项目)

本项目采用父记忆 + 子记忆双层持久化架构,两者通过共享 thread_id 实现关联,但各自独立存储:

父记忆(GlobalState) — 由 GrayRaveN 图自身的 checkpointer 持久化,以 thread_id 为键存储结构化工作流状态(userQuery、taskList、results、isFinished、isRetry 等),节点函数通过 return 显式写入更新。它是整个工作流的"战略地图",负责跨节点状态传递、重试循环的上下文锚点和中断恢复的状态保存。

子记忆(Agent Checkpointer) — 每个 Agent 在初始化时创建独立的 AsyncPostgresSaver 实例,节点函数调用 Agent 时传入与父图相同的 thread_id(见 graph.py:L125-L128 的 plan 节点、L199-L202 的 work 节点等),因此 Agent 的 checkpointer 会基于 thread_id 加载并累积该 Agent 之前的所有对话历史——重试循环中 LeaderPlanAgent 再次被调用时,能看到上一轮规划时的推理过程。但由于每个 Agent 有独立的 checkpointer 实例,Agent 之间互不可见对方的历史,天然实现了跨 Agent 的上下文隔离。

父子交互 — 节点函数充当中介:父记忆的字段通过 PromptManager 注入子 Agent 的提示词(父→子),子 Agent 的累积对话历史使其在重试场景下能感知上下文演进;子 Agent 的最终输出经 structure() 解析为 Pydantic 对象后写回父记忆(子→父),形成一条有记忆、可追溯、可回撤的闭环。

interrupt()

interrupt() 是 LangGraph 中实现人机回环(Human-in-the-Loop) 模式的核心工具。它允许你像在代码中设置一个“断点”一样,暂停图的执行,向外部(如用户或另一个系统)展示信息,并等待其输入后再继续。

interrupt() 的工作流程包含三个关键步骤:
暂停 (Pause):当图执行到节点内的 interrupt() 时,执行会被精确地挂起在该位置。它会抛出一个可恢复的异常(GraphInterrupt),并将你传给它的 value 作为“暂停载荷”(payload)返回给调用者。
持久化 (Persist):为了能稍后恢复,图在执行 interrupt() 时会通过检查点保存器(Checkpointer) 保存当前完整的状态。这是实现“断点续传”的基础,没有它就无法恢复。
恢复 (Resume):当准备好继续时,你需要用相同的 thread_id 再次调用图,并传入一个包含恢复值的 Command(resume=...)。这个值会成为 interrupt() 函数的返回值,让节点代码可以继续执行。

当图节点执行到 interrupt() 函数时,LangGraph 会在内部抛出一个 GraphInterrupt 异常。
它不是“错误”:GraphInterrupt 是一个用于控制流的内部异常(GraphBubbleUp),并非程序错误。LangGraph 的运行器(runner) 会精准地捕获它,并执行暂停逻辑,而不是将其视为失败。因此,永远不要在节点中用 try...except 捕获它,否则会破坏中断机制。(interrupt() 抛出的异常类型是 GraphInterrupt,这个类在 LangGraph 源码中直接继承自 BaseException,与 Exception平级,因此try Exception可以自然忽略GraphInterrupt)
这个异常会携带你传给 interrupt(value) 的 value,这个值会被框架保存并返回给调用者(如前端),用于展示暂停原因或请求输入。
恢复执行依赖两个关键要素:thread_id 和 Command(resume=...)。
通过 thread_id 定位断点:thread_id 是贯穿整个生命周期的“持久游标(persistent cursor)”。中断时,检查点保存器(Checkpointer)会以 thread_id 为键,保存包含精确暂停位置的完整图状态快照。恢复时,你必须使用相同的 thread_id,框架才能加载对应的检查点。
通过 Command 注入恢复值:提供 thread_id 后,通过传入 Command(resume=...) 来恢复执行。
LangGraph 会加载检查点,并从被中断节点的开头重新执行。
在重新执行过程中,你通过 Command 传入的恢复值会被注入,并作为 interrupt() 函数的返回值。节点代码拿到这个值后继续执行。

人机交互实质:抛出假异常

__interrupt__的截获:"values",其余不行。"custom"返回业务消息;value只截获__interrupt__,其余抛弃(v2版本)

        async for chunk in workGraph.astream(
            input=userInput,
            config=config,
            stream_mode=["custom","value"],
            version=2
        ):
            if chunk.type == "custom":
                yield chunk
            elif chunk.type == "value":
                if chunk.data.interrupts:
                    interrupts = chunk.data.interrupts

                    yield StreamResult(
                        event="human_interrupt_required",
                        msg="需要用户确认",
                        data={
                            "interrupts": interrupts,
                            "thread_id": config["configurable"]["thread_id"]
                        }
                    )
                else:
                    pass

兼顾机器可读、用户可读

 

用户可读:SSE 流式实时推送
StreamWriter(StreamWriter.py)拦截 Agent 的 astream_events 事件流,将 LLM 的 token 逐字以 {prefix}_thinking 事件推送给前端,工具调用以 {prefix}_tool_start / {prefix}_tool_end 播报进度,完成时以 {prefix}_done 通知。每个事件封装为 StreamResult,包含 event(事件类型)、msg(中文可读描述,如"plan 思考中"、"调用工具 xxx")和 data(结构化 payload),通过 EventSourceResponse 以标准 SSE 格式发送给前端,实现实时进度可见。

 

机器可读:Pydantic 结构化后处理
同一 LLM 的完整输出被 streamWriter.finalResult 捕获后,经 structure() 函数调用 Ollama + Outlines 强制解析为强类型 Pydantic 对象(PlanResult、WorkResult、CheckResult),这些对象直接用于更新 GlobalState、驱动条件路由(distribute、isPass)和 PostgreSQL 持久化,确保下游节点和系统能以确定的字段类型消费数据。

 

posted @ 2026-08-28 22:52  阿瓜不瓜  阅读(33)  评论(0)    收藏  举报