大模型的开发应用(十七):多智能体项目

1 项目需求

假如我想让AI生成一份关于成都市GDP增长率分析的报告,我们假设生成报告需要通过以下步骤:(1)收集资料;(2)对资料中的数据进行计算分析;(3)撰写报告。

三个步骤可以对应图中的三个节点,并且每个节点,我们都可以让大模型完成相应的功能。

本项目的工作流节点流程图如下所示:
在这里插入图片描述

图中,边上有判断条件的,表示条件边。

2 模型配置

我们先把模型推理服务吊起来:

lmdeploy serve api_server /data/coding/models/Qwen/Qwen3-4B --cache-max-entry-count 0.4 --reasoning-parser deepseek-r1

我用的是 RTX 3060,显存只有12GB,4B的模型要想能放下,需要指定–cache-max-entry-count,这个参数表示 KV Cache 占用的显存比例,默认是0.8,如果使用默认值,则会报错,说显存不够,我这里设定其为 0.4。

因为 Qwen3 是带有思维链的模型,像 deepseek 一样,但我不想打印思考过程,所以我这里需要设定 --reasoning-parser 参数为 deepseek-r1,这个参数表示将模型的输出按照 deepseek-r1 的格式来解析。

我们先把要用到的库导进来:

from typing import Literal, Annotated
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
from pydantic import BaseModel, Field
import re

然后我们进行模型配置,这里我们使用 Qwen3-4B:

# ================== 模型配置 ==================
model = ChatOpenAI(
    openai_api_base="http://localhost:23333/v1",
    model_name="/data/coding/models/Qwen/Qwen3-4B",
    openai_api_key="XXXXX",
    max_tokens=400,
    temperature=0.2,
)

3 状态类

这里我们不但要定义属性,还要定义一些方法,主要就是把字典转为状态类,或者把状态类中的属性转成字典,代码如下:

# ================== 状态类 ==================
class AgentState(BaseModel):
    # messages 用于接收上一个节点或者起始节点的信息
    messages: Annotated[list, Field(default_factory=list)]  
    # next_agent 用于指定下一个执行节点,下一个节点只能是 researcher、calculator、writer 和 done 其中之一
    next_agent: Literal["researcher", "calculator", "writer", "done"] = "researcher"   
    # step_count 用于记录步数
    step_count: int = 0
    # is_finalized 用于记录是否终止
    is_finalized: bool = False
    search_data: dict = Field(default_factory=dict)  # 新增搜索数据存储

    @classmethod
    def from_raw(cls, raw):
        # 这个函数用于判断 raw 是否为 cls 类(AgentState)对象
        # 如果 raw 属于 AgentState 类,则直接把 raw 返回,否则转成 AgentState 类对象
        """统一状态转换方法"""
        if isinstance(raw, cls):
            return raw
        if isinstance(raw, dict):
            return cls(
                messages=raw.get('messages', []),
                next_agent=raw.get('next_agent', 'researcher'),
                step_count=raw.get('step_count', 0),
                is_finalized=raw.get('is_finalized', False),
                search_data=raw.get('search_data', {})
            )
        raise ValueError(f"无效状态类型: {type(raw)}")  

    def to_safe_dict(self):
        """安全转换为字典"""
        return {
            "messages": [msg.dict() for msg in self.messages],
            "next_agent": self.next_agent,
            "step_count": self.step_count,
            "is_finalized": self.is_finalized,
            "search_data": self.search_data
        }

4 入口节点

这里我们不使用 langgraph 预定义的 SATRT,而是自己创建一个入口节点,方便进行逻辑判断。

# ================== 创建Supervisor节点(相当于 START 节点)函数 ==================
def supervisor(raw_state):
    print("================执行 supervisor 节点================")
    state = AgentState.from_raw(raw_state)
    if state.is_finalized:
        return state

    try:
        # 判断步数是否达到循环上限,若达到上限,则强行走 writer 节点
        # 因为 writer 节点执行完之后会添加 [END] 标识,相当于强制结束
        if state.step_count >= 8:   
            print("[强制终止] 达到最大步数限制")
            return AgentState(
                messages=state.messages,
                next_agent="writer",
                step_count=state.step_count + 1,
                is_finalized=False
            )

        # 消息转换,转成 langchain 能处理的格式
        messages = convert_messages(state.messages)

        # 在消息列表中添加流程控制提示词
        control_prompt = '''请严格选择下一步:\n1.需要更多数据 → researcher\n2.需要计算 → calculator \n3.生成报告 → writer\n4. 完成 → done'''
        messages = messages + [SystemMessage(content=control_prompt)]

        # 模型推理
        response = model.invoke(messages)

        # 查看模型的回复中,是否包含 researcher|calculator|writer,若不包含,则 new_agent 置为 done
        decision = re.search(r'\b(researcher|calculator|writer)\b', response.content.lower())
        new_agent = decision.group(1).lower() if decision else "done"

        # 若 new_agent 为 done,但是还没有经过 writer 节点,那只能强制调用 writer 生成报告
        # 如果经过 writer,则模型回复中会带有 [END] 字符串,因此可以据此判断是否经过 writer 节点
        if new_agent == "done" and not any("[END]" in msg.content for msg in messages):
            new_agent = "writer"

        return AgentState(
            messages=state.messages,
            next_agent=new_agent,
            step_count=state.step_count + 1,
            is_finalized=False
        )
    except Exception as e:
        # 如果发生错误,则强制结束
        print(f"[Supervisor错误] {str(e)}")
        return AgentState(
            messages=state.messages,
            next_agent="writer",
            step_count=state.step_count + 1,
            is_finalized=False
        )

这里出现了一个消息转换函数,它的功能是将消息中的字典转换为 langchain 中的消息格式:

# ================== 消息转换 ==================
def convert_messages(raw_messages):
    converted = []
    for msg in raw_messages:
        if isinstance(msg, dict):
            try:
                # 获取当前消息的角色,如果不存在,则置为 assistant
                role = msg.get("role", "assistant")
                # 获取当前消息的内容
                content = msg.get("content", "")
                # 根据角色获取类名
                role_dict = {
                    "user": HumanMessage,
                    "system": SystemMessage,
                    "assistant": AIMessage
                }
                msg_type = role_dict[role]
                # 将内容封装成 langchain 中对应角色的标准格式
                std_msg = msg_type(content=content)
                # 添加到消息列表
                converted.append(std_msg)
            except KeyError:
                converted.append(AIMessage(content=str(msg)))
        elif hasattr(msg, 'content'):
            converted.append(msg)
        else:
            converted.append(AIMessage(content=str(msg)))
    return converted

5 创建节点

因为在向图中添加节点的时候,使用的是函数名,所以如果我们使用一个函数来创建节点,那么需要使用内置函数,外层函数返回的是内层函数的函数名。

本项目中,节点与节点之间的区别在于,使用了不同的提示词,我们把提示词封装成系统消息,然后将其作为最后一条消息加入到消息列表中,随后将消息列表输入到模型中,如果当前节点是 writer,那么要给模型输出的末尾加上 [END]。下一个节点具体是什么,需要从模型的回复中解析出来,并赋值给状态类的 next_agent 变量,因此,提示词中需要让模型给出下一个节点的名称。

代码如下:

# ================== 创建普通节点函数 ==================
def create_agent(role: str, prompt: str):
    # 这里 role 不是 AI、HUMAN、SYSTEM 等角色,而是指具体哪个节点
    def agent(raw_state):
        print(f"================执行 {role} 节点================")
        state = AgentState.from_raw(raw_state)
        if state.is_finalized:
            return state

        try:
            # 消息转换
            messages = convert_messages(state.messages)

            # 往messages中添加系统信息,让节点处的模型来选择下一步该走哪个节点
            # 如果当前节点是 writer,那么 prompt 是“生成包含[END]标识的最终报告”
            messages.append(SystemMessage(
                content=f"{prompt}\n最后必须用'建议下一步:选项'格式结尾(选项只能是researcher/calculator/writer/done)"
            ))

            # 获取模型响应
            response = model.invoke(messages)
            print(f"\n[{role}输出] {response.content[:500]}...")  # 限制日志长度

            # 如果当前节点是 writer,且模型的回复中不含 [END] 标识,那就强制添加
            # 虽然在提示词中要求 writer 节点的输出以 [END],但我们使用的是小模型,小模型的指令遵循能力有限
            if role == "writer" and "[END]" not in response.content:
                response.content += "\n[END]"
                print(f"[{role}修正] 已添加[END]标识")

            # 解析下一步建议,模型未必会按照提示词来输出,所以这里可能需要强制匹配
            next_agent = "done"     # 这里先预定义下一个节点为 End
            
            # 这里使用正则匹配,:= 是海象运算符
            if match := re.search(r'建议下一步\s*[::]\s*(\w+)', response.content):
                # 从match中解析出suggestion
                suggestion = match.group(1).lower()

                # 如果suggestion在集合是 researcher、calculator、writer 其中之一,
                # 那下一个节点就按照 suggestion,否则就用预定义的节点,即 END
                if suggestion in {"researcher", "calculator", "writer", "done"}:
                    next_agent = suggestion

            return AgentState(
                messages=messages + [response],
                next_agent=next_agent,
                step_count=state.step_count + 1,
                is_finalized="[END]" in response.content    # 如果 [END] 在模型答复中,则 is_finalized 为True
            )
        except Exception as e:
            print(f"[{role}错误] {str(e)}")
            return AgentState(
                messages=state.messages,
                next_agent="done",
                step_count=state.step_count + 1,
                is_finalized=True
            )
    return agent

6 执行入口

执行入口首先要建工作流,即建图,然后添加节点,添加边,构建好后进行编译,编译结束后是流式输出。代码如下:

# ================== 执行入口 ==================
def main():
    # ================== 工作流配置(建图) ==================
    builder = StateGraph(AgentState)
    builder.add_node("supervisor", supervisor)  # supervisor 相当于开始节点,
    builder.add_node("researcher", create_agent("researcher", "收集经济数据并分析趋势"))
    builder.add_node("calculator", create_agent("calculator", "执行GDP增长率计算"))
    builder.add_node("writer", create_agent("writer", "生成包含[END]标识的最终报告"))

    # 添加条件边
    # 因为逻辑不是串行的,这里只有发起者是固定的,但边的另一端是哪个节点,需要通过状态来判断
    # 多智能体建图的时候一般都是按照下面这样的方式,即构建一条从一个节点到多个节点的条件边
    builder.add_conditional_edges(
        "supervisor",
        lambda state: state.next_agent,
        {
            "researcher": "researcher",
            "calculator": "calculator", 
            "writer": "writer",
            "done": END
        }
    )

    # 为每一个节点构建一条指向 supervisor 的边
    for agent in ["researcher", "calculator", "writer"]:
        builder.add_edge(agent, "supervisor")

    # 设定入口
    builder.set_entry_point("supervisor")

    # 编译图
    workflow = builder.compile()

    try:
        # 初始化状态时确保类型正确
        initial_state = AgentState(messages=[HumanMessage(content="成都市GDP增长率分析")])
        
        # 流式输出
        for step in workflow.stream(initial_state):
            node_name, raw_state = step.popitem()       # 这里 raw_state 是字典
            current_state = AgentState.from_raw(raw_state)
            
            # print(f"\n[系统状态] 当前节点: {node_name}")
            print(f"下一步: {current_state.next_agent}")
            print(f"步数: {current_state.step_count}")
            print(f"完成状态: {current_state.is_finalized}")

            # 检查终止条件
            if current_state.is_finalized or node_name == "__end__":
                print("\n================流程完成================")
                if any("[END]" in msg.content for msg in current_state.messages):
                    print("最终报告内容:")
                    print(next(msg.content for msg in reversed(current_state.messages) if "[END]" in msg.content))
                else:
                    print("警告:未检测到完整报告")
                break
                
    except Exception as e:
        print(f"\n!!! 流程异常终止: {str(e)}")
        if 'current_state' in locals():
            print("最后状态:", current_state.to_safe_dict())


if __name__ == "__main__":
    main()

从建图的程序可以看到,三个功能节点(researcher、writer、calculator)都有一条边指向了 supervisor,并且不是条件边,也就是说三个功能节点执行后,都会无条件执行supervisor,这使得三个功能节点执行后状态中的 next_agent 属性并不可靠。

其实也没必要让三个功能节点执行时给出下一步,因为下一步一定是 supervisor,而 next_agent 会在 supervisor 节点执行时更新,next_agent 只在 supervisor 执行完选择下一个节点时有用,因为是条件边。

当然,上面的程序,执行完 writer 节点后,就直接跳出了,没有执行 supervisor,因为已经把报告写出来了,没必要返回 supervisor。

输出:

================执行 supervisor 节点================
下一步: researcher
步数: 1
完成状态: False
================执行 researcher 节点================

[researcher输出] 

成都市GDP增长率分析(2020-2023年):  
1. **数据来源**:国家统计局、成都市统计局、《成都统计年鉴》。  
2. **趋势分析**:  
   - 2020年:受疫情影响,GDP增速降至3.2%(全国平均为2.3%),但成都通过数字经济、服务业等韧性产业快速恢复。  
   - 2021年:增速回升至6.1%,高于全国平均水平(8.1%),受益于成渝双城经济圈政策和电子信息产业扩张。  
   - 2022年:增速5.8%,略低于全国(8.1%),但成都作为西部经济高地,增速仍居全国前列。  
   - 2023年:预计增速5.5%-6%,受消费复苏和科技创新驱动,但面临房地产调整和外部环境压力。  
3. **关键驱动因素**:  
   - **产业结构升级**:电子信息、生物医药、新能源等新兴产业占比提升。  
   - **政策支持**:成渝双城经济圈建设、西部科学城规划。  
   - **消费市场**:成都作为“天府之国”,文旅、餐饮等服务业持续增长。  
4. **挑战**:房地产行业调整、外部经济不确定性、人口老龄化。  

建议下一步:**re...
下一步: done
步数: 2
完成状态: False
================执行 supervisor 节点================
下一步: writer
步数: 3
完成状态: False
================执行 writer 节点================

[writer输出] 

**成都市GDP增长率分析报告(2020-2023年)**  
[END]  

**核心结论**:  
- 成都市GDP年均增速稳定在5.5%-6.1%,高于全国平均水平(2020-2023年全国平均增速约4.5%)。  
- 2023年增速预计5.5%-6%,受消费复苏、科技创新和成渝双城经济圈政策推动,但面临房地产调整和外部环境压力。  
- 产业结构持续优化,电子信息、生物医药、新能源等新兴产业贡献率显著提升。  

**建议下一步:**  
done...
下一步: done
步数: 4
完成状态: True

================流程完成================
最终报告内容:


**成都市GDP增长率分析报告(2020-2023年)**  
[END]  

**核心结论**:  
- 成都市GDP年均增速稳定在5.5%-6.1%,高于全国平均水平(2020-2023年全国平均增速约4.5%)。  
- 2023年增速预计5.5%-6%,受消费复苏、科技创新和成渝双城经济圈政策推动,但面临房地产调整和外部环境压力。  
- 产业结构持续优化,电子信息、生物医药、新能源等新兴产业贡献率显著提升。  

**建议下一步:**  
done

上面的 “最终报告内容” 之所以在 成都市GDP增长率分析报告(2020-2023年 后面会有个 [END] 标识符,是因为在执行 writer 节点的时候,我们的提示词为 生成包含[END]标识的最终报告,结果生成的 [END] 标识符并不是在末尾,而是在标题的中间,从打印结果也能看到:

在这里插入图片描述

7 调用商业模型实现

这里我们调用智谱商业大模型,模型配置的代码修改如下:

# ================== 模型配置 ==================
model = ChatOpenAI(
    openai_api_base="https://open.bigmodel.cn/api/paas/v4",
    model_name="glm-4-plus",
    openai_api_key="XXXXX",		# 去智谱官网申请 API Key
    max_tokens=400,
    temperature=0.2
)

输出:

================执行 supervisor 节点================
下一步: writer
步数: 1
完成状态: False
================执行 writer 节点================

[writer输出] 成都市作为中国西南地区的重要经济中心,其GDP增长率一直是衡量当地经济发展状况的重要指标。以下是对成都市GDP增长率的分析,主要从历史数据、影响因素和未来趋势三个方面进行探讨。

### 一、历史数据分析

1. **近年GDP增长率**:
   - **2015-2020年**:这一时期,成都市的GDP增长率基本保持在7%-8%之间,显示出较为稳健的增长态势。其中,2018年和2019年受全球经济形势和国内经济结构调整的影响,增长率略有放缓。
   - **2020年**:受新冠疫情影响,成都市GDP增长率有所下降,但仍然保持在正增长区间,显示出较强的经济韧性。
   - **2021-2022年**:随着疫情逐步得到控制和经济恢复,成都市的GDP增长率有所回升,重回7%以上的增长水平。

2. **产业结构变化**:
   - **第一产业**:占比逐年下降,但对农业现代化的投入增加,提升了农业产值。
   - **第二产业**:工业和服务业成为拉动GDP增长的主要动力,特别是电子信息、汽车制造等高新技术产业的快速发展。
   - **第三产业**:服务业占比持续提升,尤其是金融、...
[writer修正] 已添加[END]标识
下一步: done
步数: 2
完成状态: True

================流程完成================
最终报告内容:
成都市作为中国西南地区的重要经济中心,其GDP增长率一直是衡量当地经济发展状况的重要指标。以下是对成都市GDP增长率的分析,主要从历史数据、影响因素和未来趋势三个方面进行探讨。

### 一、历史数据分析

1. **近年GDP增长率**:
   - **2015-2020年**:这一时期,成都市的GDP增长率基本保持在7%-8%之间,显示出较为稳健的增长态势。其中,2018年和2019年受全球经济形势和国内经济结构调整的影响,增长率略有放缓。
   - **2020年**:受新冠疫情影响,成都市GDP增长率有所下降,但仍然保持在正增长区间,显示出较强的经济韧性。
   - **2021-2022年**:随着疫情逐步得到控制和经济恢复,成都市的GDP增长率有所回升,重回7%以上的增长水平。

2. **产业结构变化**:
   - **第一产业**:占比逐年下降,但对农业现代化的投入增加,提升了农业产值。
   - **第二产业**:工业和服务业成为拉动GDP增长的主要动力,特别是电子信息、汽车制造等高新技术产业的快速发展。
   - **第三产业**:服务业占比持续提升,尤其是金融、物流、文化旅游等现代服务业的增长显著。

### 二、影响因素分析

1. **政策支持**:
   - 国家对西部地区的扶持政策,如西部大开发战略,为成都市经济发展提供了政策红利。
   - 成都市政府出台的一系列招商引资、科技创新和产业升级政策,有效推动了经济增长。

2. **区位优势**:
   - 成都市地处西南地区中心,交通便利,是连接西南、西北和华中地区的重要枢纽,区位优势明显。
   - 成都天府国际机场的建成投运,进一步提升了成都的国际化水平和经济辐射能力。

3. **产业结构优化**:
   - 高新技术产业的快速发展,特别是电子信息、生物医药等领域的突破,为经济增长注入新动力。
   - 传统产业的转型升级,提升了产业链的整体竞争力。

4. **人口红利**:
   - 成都市作为人口净流入城市,劳动力资源丰富,消费市场潜力巨大。

### 三、未来趋势预测

1. **持续增长**:
   - 预计未来几年,成都市的GDP增长率将保持在6%-8%之间,继续保持稳健增长态势。

2. **产业结构进一步优化**:
   - 高新技术产业和服务业将继续成为经济增长的主要驱动力,传统产业将进一步转型升级。

3. **区域协同发展**:
   - 成渝地区双城经济圈建设的推进,将为成都市带来更多发展机遇,促进区域经济协同发展。

4. **绿色发展**:
   - 随着环保意识的增强和绿色政策的实施,成都市将更加注重可持续发展,绿色经济将成为新的增长点。

### 结论

总体来看,成都市GDP增长率在过去几年中表现出较强的稳定性和韧性,未来在政策支持、区位优势和产业结构优化的共同作用下,预计将继续保持稳健增长。同时,成都市应继续加强科技创新、优化营商环境、推动绿色发展,以实现更高质量的经济增长。
[END]

可以看到,调用商业模型后,程序只用了两步就完成了报告,并且跳过了收集数据的步骤,而是过了 supervisor 后直接走 writer 节点,然后结束。

当然,虽然glm-4-plus模型的能力确实比较强,但在 writer 节点中,也并未严格按照提示词生成 [END] 标识,另外,glm-4-plus 模型预训练用的数据比较老了,生成的报告只有2022年的。

8 总结

本项目还是比较复杂的,主要是 supervisor 和 create_agent 这两个函数比较复杂,需要比较长的时间才能把逻辑理顺,但只要把这两个函数搞明白了,基本上这个项目就梳理清楚了。

一般情况下,如果存在条件边,让模型去做判断下一个节点走哪里,就需要选择适当的提示词(比如在 supervisor 节点中的提示词,让模型选择下一个节点走哪里),把所有可能的情况都写到提示词里,最后根据模型的回复来确定走哪个节点。因为要保证模型能尽量按指令执行,并且具有较强的选择能力,那么必须在条件边的出点(本项目为 supervisor 节点)使用推理能力比较强的模型,同时在程序中把规则写死,防止模型给出的选择结果不在我们提供的选项范围内,依次做到流程控制。

附录:完整代码

from typing import Literal, Annotated
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
from pydantic import BaseModel, Field
import re

# ================== 模型配置 ==================
model = ChatOpenAI(
    openai_api_base="http://localhost:23333/v1",
    model_name="/data/coding/models/Qwen/Qwen3-4B",
    openai_api_key="XXXXX",
    max_tokens=400,
    temperature=0.2,
)


# ================== 状态类 ==================
class AgentState(BaseModel):
    # messages 用于接收上一个节点或者起始节点的信息
    messages: Annotated[list, Field(default_factory=list)]  
    # next_agent 用于指定下一个执行节点,下一个节点只能是 researcher、calculator、writer 和 done 其中之一
    next_agent: Literal["researcher", "calculator", "writer", "done"] = "researcher"   
    # step_count 用于记录步数
    step_count: int = 0
    # is_finalized 用于记录是否终止
    is_finalized: bool = False
    search_data: dict = Field(default_factory=dict)  # 新增搜索数据存储

    @classmethod
    def from_raw(cls, raw):
        # 这个函数用于判断 raw 是否为 cls 类对象,这里 cls 是类名,比如 AgentState
        # 如果 raw 属于 AgentState 类,则直接把 raw 返回,否则转成 AgentState 类对象
        """统一状态转换方法"""
        if isinstance(raw, cls):
            return raw
        if isinstance(raw, dict):
            return cls(
                messages=raw.get('messages', []),
                next_agent=raw.get('next_agent', 'researcher'),
                step_count=raw.get('step_count', 0),
                is_finalized=raw.get('is_finalized', False),
                search_data=raw.get('search_data', {})
            )
        raise ValueError(f"无效状态类型: {type(raw)}")  

    def to_safe_dict(self):
        """安全转换为字典"""
        return {
            "messages": [msg.dict() for msg in self.messages],
            "next_agent": self.next_agent,
            "step_count": self.step_count,
            "is_finalized": self.is_finalized,
            "search_data": self.search_data
        }


# ================== 创建Supervisor节点(相当于 START 节点)函数 ==================
def supervisor(raw_state):
    print("================执行 supervisor 节点================")
    state = AgentState.from_raw(raw_state)
    if state.is_finalized:
        return state

    try:
        # 判断步数是否达到循环上限,若达到上限,则强行走 writer 节点
        # 因为 writer 节点执行完之后会添加 [END] 标识,相当于强制结束
        if state.step_count >= 8:   
            print("[强制终止] 达到最大步数限制")
            return AgentState(
                messages=state.messages,
                next_agent="writer",
                step_count=state.step_count + 1,
                is_finalized=False
            )

        # 消息转换,转成 langchain 能处理的格式
        messages = convert_messages(state.messages)

        # 在消息列表中添加流程控制提示词
        control_prompt = '''请严格选择下一步:\n1.需要更多数据 → researcher\n2.需要计算 → calculator \n3.生成报告 → writer\n4. 完成 → done'''
        messages = messages + [SystemMessage(content=control_prompt)]

        # 模型推理
        response = model.invoke(messages)

        # 查看模型的回复中,是否包含 researcher|calculator|writer,若不包含,则 new_agent 置为 done
        decision = re.search(r'\b(researcher|calculator|writer)\b', response.content.lower())
        new_agent = decision.group(1).lower() if decision else "done"

        # 若 new_agent 为 done,但是还没有经过 writer 节点,那只能强制调用 writer 生成报告
        # 如果经过 writer,则模型回复中会带有 [END] 字符串,因此可以据此判断是否经过 writer 节点
        if new_agent == "done" and not any("[END]" in msg.content for msg in messages):
            new_agent = "writer"

        return AgentState(
            messages=state.messages,
            next_agent=new_agent,
            step_count=state.step_count + 1,
            is_finalized=False
        )
    except Exception as e:
        # 如果发生错误,则强制结束
        print(f"[Supervisor错误] {str(e)}")
        return AgentState(
            messages=state.messages,
            next_agent="writer",
            step_count=state.step_count + 1,
            is_finalized=False
        )


# ================== 消息转换 ==================
def convert_messages(raw_messages):
    converted = []
    for msg in raw_messages:
        if isinstance(msg, dict):
            try:
                # 获取当前消息的角色,如果不存在,则置为 assistant
                role = msg.get("role", "assistant")
                # 获取当前消息的内容
                content = msg.get("content", "")
                # 根据角色获取类名
                role_dict = {
                    "user": HumanMessage,
                    "system": SystemMessage,
                    "assistant": AIMessage
                }
                msg_type = role_dict[role]
                # 将内容封装成 langchain 中对应角色的标准格式
                std_msg = msg_type(content=content)
                # 添加到消息列表
                converted.append(std_msg)
            except KeyError:
                converted.append(AIMessage(content=str(msg)))
        elif hasattr(msg, 'content'):
            converted.append(msg)
        else:
            converted.append(AIMessage(content=str(msg)))
    return converted


# ================== 创建普通节点函数 ==================
def create_agent(role: str, prompt: str):
    # 这里 role 不是 AI、HUMAN、SYSTEM 等角色,而是指具体哪个节点
    def agent(raw_state):
        print(f"================执行 {role} 节点================")
        state = AgentState.from_raw(raw_state)
        if state.is_finalized:
            return state

        try:
            # 消息转换
            messages = convert_messages(state.messages)

            # 往messages中添加系统信息,让节点处的模型来选择下一步该走哪个节点
            # 如果当前节点是 writer,那么 prompt 是 生成包含[END]标识的最终报告
            messages.append(SystemMessage(
                content=f"{prompt}\n最后必须用'建议下一步:选项'格式结尾(选项只能是researcher/calculator/writer/done)"
            ))

            # 获取模型响应
            response = model.invoke(messages)
            print(f"\n[{role}输出] {response.content[:500]}...")  # 限制日志长度

            # 如果当前节点是 writer,且模型的回复中不含 [END] 标识,那就强制添加
            # 虽然在提示词中要求 writer 节点的输出以 [END],但我们使用的是小模型,小模型的指令遵循能力有限
            if role == "writer" and "[END]" not in response.content:
                response.content += "\n[END]"
                print(f"[{role}修正] 已添加[END]标识")

            # 解析下一步建议,模型未必会按照提示词来输出,所以这里可能需要强制匹配
            next_agent = "done"     # 这里先预定义下一个节点为 End
            
            # 这里使用正则匹配,:= 是海象运算符
            if match := re.search(r'建议下一步\s*[::]\s*(\w+)', response.content):
                # 从match中解析出suggestion
                suggestion = match.group(1).lower()

                # 如果suggestion在集合是 researcher、calculator、writer 其中之一,
                # 那下一个节点就按照 suggestion,否则就用预定义的节点,即 END
                if suggestion in {"researcher", "calculator", "writer", "done"}:
                    next_agent = suggestion

            return AgentState(
                messages=messages + [response],
                next_agent=next_agent,
                step_count=state.step_count + 1,
                is_finalized="[END]" in response.content    # 如果 [END] 在模型答复中,则 is_finalized 为True
            )
        except Exception as e:
            print(f"[{role}错误] {str(e)}")
            return AgentState(
                messages=state.messages,
                next_agent="done",
                step_count=state.step_count + 1,
                is_finalized=True
            )
    return agent


# ================== 执行入口 ==================
def main():
    # ================== 工作流配置(建图) ==================
    builder = StateGraph(AgentState)
    builder.add_node("supervisor", supervisor)  # supervisor 相当于开始节点,
    builder.add_node("researcher", create_agent("researcher", "收集经济数据并分析趋势"))
    builder.add_node("calculator", create_agent("calculator", "执行GDP增长率计算"))
    builder.add_node("writer", create_agent("writer", "生成包含[END]标识的最终报告"))

    # 添加条件边
    # 因为逻辑不是串行的,这里只有发起者是固定的,但边的另一端是哪个节点,需要通过状态来判断
    # 多智能体建图的时候一般都是按照下面这样的方式,即构建一条从一个节点到多个节点的条件边
    builder.add_conditional_edges(
        "supervisor",
        lambda state: state.next_agent,
        {
            "researcher": "researcher",
            "calculator": "calculator", 
            "writer": "writer",
            "done": END
        }
    )

    # 为每一个节点构建一条指向 supervisor 的边
    for agent in ["researcher", "calculator", "writer"]:
        builder.add_edge(agent, "supervisor")

    # 设定入口
    builder.set_entry_point("supervisor")

    # 编译图
    workflow = builder.compile()

    try:
        # 初始化状态时确保类型正确
        initial_state = AgentState(messages=[HumanMessage(content="成都市GDP增长率分析")])
        
        # 流式输出
        for step in workflow.stream(initial_state):
            node_name, raw_state = step.popitem()       # 这里 raw_state 是字典
            current_state = AgentState.from_raw(raw_state)
            
            # print(f"\n[系统状态] 当前节点: {node_name}")
            print(f"下一步: {current_state.next_agent}")
            print(f"步数: {current_state.step_count}")
            print(f"完成状态: {current_state.is_finalized}")

            # 检查终止条件
            if current_state.is_finalized or node_name == "__end__":
                print("\n================流程完成================")
                if any("[END]" in msg.content for msg in current_state.messages):
                    print("最终报告内容:")
                    print(next(msg.content for msg in reversed(current_state.messages) if "[END]" in msg.content))
                else:
                    print("警告:未检测到完整报告")
                break
                
    except Exception as e:
        print(f"\n!!! 流程异常终止: {str(e)}")
        if 'current_state' in locals():
            print("最后状态:", current_state.to_safe_dict())


if __name__ == "__main__":
    main()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值