在这个教程中,我将向您展示如何在不使用任何编排框架的情况下,用Python构建多智能体AI系统。我们还会利用LangGraph中的节点、边和共享状态来实现这一目标。

之所以要同时制作这两个版本,就是为了让您了解使用框架与不使用框架之间的区别。

简单的Python版本可以展示:实际上只需要很少的代码就能构建出一个多智能体系统;而LangGraph版本则能体现工作流框架在构建这类系统时所发挥的作用。

这些智能体会通过Ollama和Qwen在本地运行,因此您无需支付任何API使用费用。

目录

背景知识

大型语言模型能够通过一个简单的指令来完成极其复杂的任务,对于许多应用来说,这种处理方式确实非常合适。

然而,当工作流程变得越来越复杂时,单个指令往往需要同时完成多项任务。将所有这些功能整合到同一个指令中,会使得系统的维护、扩展以及问题分析变得更加困难,尤其是对于那些内存资源有限的本地模型而言。

一种常见的解决方案就是将整个工作流程分解成多个较小的步骤,从而构建多智能体系统,而不是让单个智能体来承担全部任务。

要完成这个教程,您需要在自己的机器上安装Ollama,并拥有一个免费的Ollama账户。本教程适用于macOS、Windows和Linux操作系统。我使用的是一台配备32GB内存的MacBook Pro,但如果您使用的是内存较小的设备,也可以选择Ollama中配置较低内存要求的Qwen模型来运行这个教程。

什么是多智能体系统?

在这个教程中,多智能体系统简单来说就是由多个智能体协作共同完成某项任务的系统。

每个智能体都具有以下特点:

  • 特定的职责

  • 属于自己的指令和操作指南

  • 在整体工作流程中明确的定位

与让单个模型来解决整个问题相比,多智能体系统会将任务分解成多个更具体、更专注的小任务。由于每个智能体的目标都比较明确,因此它所接收的指令通常也会更加简单,模型也更容易持续地按照这些指令来执行操作。

本教程有意将系统设计得较为简单。其中不涉及内存管理、工具调用或复杂的设计模式,而是通过一个简单的应用场景来展示多智能体人工智能系统的基本构成要素。

何时使用多智能体系统

当某项任务可以自然地划分为不同的步骤或角色时,使用多智能体系统会非常合适。例如,在规划、写作、审阅等环节中,或者在工作流程的不同阶段使用不同的专用提示时,多智能体系统就能发挥重要作用。如果单个智能体在明确的提示下能够顺利完成任务,并且输出结果稳定可靠,那么增加更多智能体反而只会引入额外的复杂性、延迟和开销。

一般来说,当职责的分工能够显著提升任务效果时,就应该使用多智能体系统;而当任务仍可以作为一个连贯的整体来处理时,使用单个智能体就更为合适。

设计动机与架构

在本教程中,我们将利用小型Qwen本地大语言模型和Ollama来构建一个简单的人工智能学习指南生成器。给定一个主题作为输入,该系统会生成一份包含大纲、笔记和复习题的结构化学习指南。单个智能体的输入格式如下:

为这个主题创建一份适合初学者的学习指南:{topic}

输出内容必须包含以下部分:

1. 大纲
  - 将该主题分为3个简短的学习章节

2. 笔记
  - 为每个章节撰写简洁明了的学习笔记
  - 确保解释清晰易懂

3. 复习题
  - 根据笔记内容编写3道简短的复习题

请以干净的Markdown格式返回结果。

单个智能体需要同时完成多项任务才能根据上述输入生成学习指南。对于一个规模较小的本地模型来说,一次性处理这么多任务确实相当困难,因此输出的质量也可能不尽如人意。

多智能体系统可以通过将这个复杂的任务分解为三个专门的子任务来帮助解决问题。这样,小型模型就能更轻松地完成各项任务。工作流程中的智能体角色如下:

  • 规划者:将主题划分为合理的章节结构。

  • 笔记编写者:为每个章节撰写简洁的学习笔记。

  • 复习题生成器:根据笔记内容生成复习题以巩固学习效果。

这种工作流程可以通过两种方式来实现。在简单的Python版本中,Python代码会负责协调各个步骤并调用相应的智能体;而在LangGraph版本中,同样的流程则是通过节点、边和共享状态来表示的。智能体本身并没有发生变化,而LangGraph则将整个工作流程建模为一个图结构。每个节点负责完成一项任务,更新共享状态,然后将状态传递给下一个节点,最终得到输出结果。

步骤1:安装Ollama及相关依赖项

首先安装Ollama并下载相应的模型:

ollama pull qwen3.5:4b

接下来配置Python开发环境:

python3 -m venv venv
source venv/bin/activate
pip install langchain-ollama langgraph

步骤 2:简单的 Python 版本

这个简单的 Python 版本使用了三个专门的 LLM 功能或代理(规划器、教师和测验生成器),这些功能通过普通的 Python 代码进行协调。

`ask()` 函数将系统提示和用户输入发送给模型,然后返回模型的响应文本。`run_agent()` 函数负责执行这个调用过程,并记录每个步骤所花费的时间。

接下来,代码定义了三个具有各自特定功能的代理:

  • `planner_agent()` 会为某个主题生成一个三部分的提纲。

  • `teacher_agent()` 会将这个提纲转化为适合初学者的简短笔记。

  • `quiz_agent()` 会根据这些笔记生成三道复习题。

`build_study_guide()` 函数会按顺序运行这三个代理,将每个代理的输出结果传递给下一个步骤。

请将这段代码保存为 study_guide_v1.py 文件。

import time
from langchain_ollama import ChatOllama

# 三个代理都使用的本地 Ollama 模型。
MODEL = ChatOllama(model="qwen3.5:4b", temperature=0)

def ask(system: str, user: str) -> str:
"""使用系统提示和用户输入来调用 LLM 模型."""
response = MODEL.invoke([
{"role": "system", "content": system},
{"role": "user", "content": user},
])
return response.content

def run_agent(name: str, system: str, user: str) -> str:
"""用于记录每个代理执行所需时间的辅助函数."""
print(f"正在调用代理 {name}...")
start = time.time()
result = ask(system, user)
print(f"{name} 执行完成,耗时 {time.time() - start:.1f} 秒")
return result

# 代理 1:生成简短的提纲
def planner_agent(topic: str) -> str:
return run_agent(
"planner_agent",
"将这个主题分为三个部分进行学习。",
topic,
)

# 代理 2:将提纲转化为笔记
def teacher_agent(topic: str, outline: str) -> str:
return run_agent(
"teacher_agent",
"根据提纲编写适合初学者的简短笔记,注意保持简洁。",
f"主题:{topic}\n\n提纲:\n{outline}",
)

# 代理 3:根据笔记生成复习题
def quiz_agent(topic: str, notes: str) -> str:
return run_agent(
"quiz_agent",
"根据这些笔记编写三道复习题。",
f"主题:{topic}\n\n笔记:\n{notes}",
)

def build_study_guide(topic: str) -> str:
"""按顺序运行三个代理,并合并它们的输出结果."""
outline = planner_agent(topic)
notes = teacher_agent(topic, outline)
quiz = quiz_agent(topic, notes)

return (
f"# 学习指南:{topic}\n\n"
f"## 提纲\n{outline}\n\n"
f"## 笔记\n{notes}\n\n"
f"## 复习题\n{quiz}\n"
)

if __name__ == "__main__":
print("正在预热模型...")
MODEL.invoke("Say ready.")
print("模型已准备就绪。\n")

topic = input("请输入学习主题:").strip()
print("\n" + build_study_guide(topic))

运行它:

python study_guide_v1.py

这样,一个能够正常运行的多智能体系统就搭建完成了。每个智能体实际上都只是对某个大型语言模型的调用;Python负责协调这些调用的顺序,因此根本不需要任何额外的框架。对于这种具有固定执行流程的系统来说,使用纯Python通常就是最好的选择。

步骤3:带有节点和边的LangGraph版本

现在让我们用LangGraph来构建同样的学习笔记生成系统。各个智能体的角色保持不变,但LangGraph会负责整个系统的协调工作:

  • 每个专家都对应一个节点

  • 所有智能体共享的数据结构被称为图的状态

  • 这些智能体的执行顺序则通过来表示

与之前需要手动按顺序调用各个智能体的方式不同,现在整个流程被定义为一个图:START -> planner -> teacher -> quiz -> END

每个节点都会从图的状态中读取数据,然后只返回自己需要更新的部分。

将这段代码保存为study_guide_v2.py

from typing import TypedDict
import time

from langchain_ollama import ChatOllama
from langgraph.graph import StateGraph, START, END

# 所有节点都会使用的本地Ollama模型。
MODEL = ChatOllama(model="qwen3.5:4b", temperature=0)

# 被所有节点共享的状态数据结构。
class StudyState(TypedDict):
    topic: str
    outline: str
    notes: str
    quiz: str

def asksystem: str, user: str) -> str:
    response = MODEL.invoke([
        {"role": "system", "content": system},
        {"role": "user", "content": user},
    ])
    return response.content

def run_node(name: str, system: str, user: str) -> str:
    print(f"正在调用节点 {name}...")
    start = time.time()
    result = asksystem, user)
    print(f"{name} 的执行耗时:{time.time() - start:.1f}秒")
    return result

# 节点1:生成学习大纲
def planner(state: StudyState) -> dict:
    return {
        "outline": run_node(
            "planner",
            "将这个主题分为3个简短的学习部分。",
            state["topic"],
        )
    }

# 节点2:根据大纲编写学习笔记
def teacher(state: StudyState) -> dict:
    return {
        "notes": run_node(
            "teacher",
            "使用大纲内容编写简洁易懂的笔记。",
            f"主题:{state['topic']}\n\n大纲:\n{state['outline']}",
        )
    }

# 节点3:根据笔记生成复习题
def quiz_writer(state: StudyState) -> dict:
    return {
        "quiz": run_node(
            "quiz_writer",
            "根据笔记内容编写3道简短的复习题。",
            f"主题:{state['topic']}\n\n笔记:\n{state['notes']}",
        )
    }

def build_graph():
    graph = StateGraph(StudyState)

    # 添加各个节点
    graph.add_node("planner", planner)
    graph.add_node("teacher", teacher)
    graph.add_node("quiz_writer", quiz_writer)

    # 定义执行顺序
    graph.add_edge(START, "planner")
    graph.add_edge("planner", "teacher")
    graph.add_edge("teacher", "quiz_writer")
    graph.add_edge("quiz_writer", END)

    return graph.compile()

if __name__ == "__main__":
    print("正在预热模型...")
    MODEL.invoke("Say ready.")
    print("模型已准备就绪。\n")

    app = build_graph()
    topic = input("请输入学习主题:").strip()

    result = app.invoke({
        "topic": topic,
        "outline": "",
        "notes": "",
        "quiz": "",
    })

    print(
        f"\n# 学习指南:{topic}\n\n"
        f"## 大纲\n{result['outline']}\n\n"
        f"## 笔记\n{result['notes']}\n\n"
        f"## 复习题\n{result['quiz']}\n"
    )

运行方法:

python study_guide_v2.py

无论是简单的Python版本,还是LangGraph版本,这两段代码都在执行相同的核心功能:通过协调多个由大语言模型驱动的步骤来完成复杂的任务。

对于那些流程简单、呈线性结构的情况,使用普通的Python代码通常是最佳选择。

但当工作流程需要处理共享状态、分支逻辑、循环结构,或者需要更复杂的代理协调时,LangGraph就会显得更加适用。

示例输出结果

对于以下输入内容:

请输入学习主题:牛顿运动定律

无论是哪个版本,最终都会生成相同类型的输出结果:一份包含章节内容、笔记以及复习题的学习指南。

典型的输出结果可能如下所示:

常见的多智能体模式

本教程中的示例是一种顺序处理流程:各个专家按固定顺序将工作交给下一个专家。这是开始学习多智能体系统时最容易理解的模式,但并非唯一的模式。

有几种值得了解的模式如下:

  • 并行处理的专家们:多个智能体独立地对相同输入进行处理,最后将它们的输出结果合并起来。

  • 协调者与子智能体:一个顶层智能体会将任务分解成若干部分,然后分配给相应的子智能体去执行,最后再汇总所有结果。

  • 监督者/路由器:这个智能体会决定哪个专家应该处理某个请求。

  • 人工干预机制:某个智能体会先初步完成工作,但最终需要由人类进行审核或批准后才能继续下一步流程。

  • 评审与优化循环:一个智能体生成结果,另一个智能体会对这些结果进行检查或改进。

下面这张信息图以可视化的方式展示了这些模式:

顺序处理流程:工作按固定顺序在专家之间传递。并行处理的专家们:多个智能体独立处理相同输入,然后合并结果。当子任务之间没有相互依赖关系时,这种模式效果很好。协调者与子智能体:顶层智能体会将任务分解,分配给子智能体执行,最后汇总结果。当需要一个智能体来协调其他多个智能体时,这种模式非常有用。监督者/路由器:这个智能体会决定哪个专家应该处理某个请求。当工作流程取决于输入类型而非固定顺序时,这种模式很适用。人工干预机制:某个智能体会先初步完成工作,但最终需要人类审核才能继续。对于敏感或面向用户的输出内容,这种模式通常是最合适的。评审与优化循环:一个智能体生成结果,另一个智能体会对其进行改进或检查。当质量比速度更重要时,这种模式非常有用;不过对于一些规模较小的本地模型来说,这种方式可能会增加处理难度。

结论

通过本教程,我们使用Python语言构建了一个简单的多智能体AI系统,既使用了LangGraph框架,也尝试了不使用该框架的情况。

接下来,你可以尝试扩展这个示例:添加一个将笔记内容简化处理的节点;加入一个审核步骤,用来检查测验结果是否与笔记内容一致;或者对图形结构进行调整,使得初级主题能得到比高级主题更简单的解释。祝你愉快地探索和学习吧!

如果你喜欢本教程,可以在我的博客中找到我的更多文章(最近的文章包括一系列系统设计论文);也可以在我的个人网站上查看我的工作成果;同时,你还可以在LinkedIn上关注我的动态。

Comments are closed.