100行代码搞定多智能体?这个极简AI框架PocketFlow有点东西 原创

发布于 2025-7-2 06:00
浏览
0收藏

现在各种工作流框架太多了,看不过来,也没有什么精力去学习。最近无意中刷到一个微型框架:PocketFlow,这个框架非常小巧,看了下只有 100 行左右的代码,很容易看懂。我非常喜欢,写个教程介绍一下。

对比其他框架


 抽象

应用特定包装器

供应商特定包装器

代码行数

大小

LangChain

Agent, Chain

很多 (例如,QA, 摘要)

很多 (例如,OpenAI, Pinecone等)

405K

+166MB

CrewAI

Agent, Chain

很多 (例如,FileReadTool, SerperDevTool)

很多 (例如,OpenAI, Anthropic, Pinecone等)

18K

+173MB

SmolAgent

Agent

一些 (例如,CodeAgent, VisitWebTool)

一些 (例如,DuckDuckGo, Hugging Face等)

8K

+198MB

LangGraph

Agent, Graph

一些 (例如,语义搜索)

一些 (例如,PostgresStore, SqliteSaver等)

37K

+51MB

AutoGen

Agent

一些 (例如,Tool Agent, Chat Agent)

很多 [可选] (例如,OpenAI, Pinecone等)

7K (仅核心)

+26MB (仅核心)

PocketFlow

Graph

100

+56KB

100行代码搞定多智能体?这个极简AI框架PocketFlow有点东西-AI.x社区

通过对比可以看到,PocketFlow 没有很多融合的功能,只抽象出Graph,就能完成常见的RAG和Agent相关的功能了。

应用场景

PocketFlow 设计了多种应用场景的示例,从基础的聊天机器人到复杂的多智能体系统。以下是一些基础示例:

  1. 聊天:基础聊天机器人,带有对话历史
  2. 结构化输出:通过提示从简历中提取结构化数据
  3. 工作流:写作工作流,包括大纲、内容编写和样式应用
  4. 智能体:可以搜索网络并回答问题的研究智能体
  5. RAG:简单的检索增强生成过程
  6. 批处理:将 Markdown 内容翻译成多种语言的批处理器

更复杂的应用包括:多智能体通信、监督流程、并行执行、思维链推理、短期和长期记忆聊天机器人等。

多智能体实战

现在我们用PocketFlow实现一个AI版"你画我猜"——Taboo游戏

为了直观地展示PocketFlow的威力,我们来看一个用它实现的多智能体协作案例:Taboo(禁忌语)游戏

游戏规则:

*   提示者 (Hinter): 知道一个目标词和几个"禁忌词"。它的任务是给出提示,引导猜词者猜出目标词,但提示中不能包含任何禁忌词。

*   猜词者 (Guesser): 根据提示者的提示,猜出目标词。

在这个案例中,两个LLM将分别扮演提示者和猜词者,它们需要通过不断的异步通信来协作完成游戏。

PocketFlow使用​​AsyncNode​​​来定义异步任务。我们的两个智能体​​AsyncHinter​​​和​​AsyncGuesser​​都继承自它。

提示者 AsyncHinter

class AsyncHinter(AsyncNode):
    asyncdef prep_async(self, shared):
        # 1. 从队列中等待猜词者的消息
        guess = await shared["hinter_queue"].get()
        if guess == "GAME_OVER":
            returnNone
        # 2. 准备LLM的输入
        return shared["target_word"], shared["forbidden_words"], shared.get("past_guesses", [])

    asyncdef exec_async(self, inputs):
        # ... (调用LLM生成提示)
        target, forbidden, past_guesses = inputs
        prompt = f"Generate hint for '{target}'\nForbidden words: {forbidden}"
        if past_guesses:
            prompt += f"\nPrevious wrong guesses: {past_guesses}\nMake hint more specific."
        hint = call_llm(prompt)
        print(f"\nHinter: Here's your hint - {hint}")
        return hint

    asyncdef post_async(self, shared, prep_res, exec_res):
        if exec_res isNone:
            return"end"
        # 3. 将生成的提示放入猜词者的队列
        await shared["guesser_queue"].put(exec_res)
        return"continue"# 返回Action,驱动流程继续

猜词者 AsyncGuesser

class AsyncGuesser(AsyncNode):
    asyncdef prep_async(self, shared):
        # 1. 从队列中等待提示者的提示
        hint = await shared["guesser_queue"].get()
        return hint, shared.get("past_guesses", [])

    asyncdef exec_async(self, inputs):
        # ... (调用LLM生成猜测)
        hint, past_guesses = inputs
        prompt = f"Given hint: {hint}, past wrong guesses: {past_guesses}, make a new guess."
        guess = call_llm(prompt)
        print(f"Guesser: I guess it's - {guess}")
        return guess

    asyncdef post_async(self, shared, prep_res, exec_res):
        # 2. 检查答案
        if exec_res.lower() == shared["target_word"].lower():
            print("Game Over - Correct guess!")
            await shared["hinter_queue"].put("GAME_OVER")
            return"end"
        
        # 3. 如果猜错,将错误答案发回给提示者,以便其给出更好的提示
        shared.setdefault("past_guesses", []).append(exec_res)
        await shared["hinter_queue"].put(exec_res)
        return"continue"

两个智能体通过共享存储​​shared​​​中的两个​​asyncio.Queue​​​(​​hinter_queue​​​和​​guesser_queue​​)进行异步通信,一个用于接收信息,一个用于发送信息,实现了完美的解耦。

async def main():
    # ... (初始化shared, 包括target_word, forbidden_words, 和两个queue)

    # 创建节点和流
    hinter = AsyncHinter()
    guesser = AsyncGuesser()

    hinter_flow = AsyncFlow(start=hinter)
    guesser_flow = AsyncFlow(start=guesser)

    # 定义循环:当post返回"continue"时,节点会再次执行自己
    hinter - "continue" >> hinter
    guesser - "continue" >> guesser

    # 使用asyncio.gather并发运行两个智能体流
    await asyncio.gather(
        hinter_flow.run_async(shared),
        guesser_flow.run_async(shared)
    )

每个智能体都被包装在一个独立的​​AsyncFlow​​​中,并通过​​"continue"​​这个Action实现自我循环,不断地接收、处理、发送消息。

核心理念

PocketFlow 把 LLM 工作流抽象为:

+-----------+
  Shared   |           |
  Store <--|   Node    |<-- Params(仅 Batch 用)
           +-----------+
                |
             Action
                v
           +-----------+
           |   Node    |
           +-----------+
  • Node:执行 prep → exec → post 三段式。
  • Action:post() 返回字符串,决定流向哪一个 successor。
  • Flow:负责“根据 Action 走图”的调度器。
  • Shared Store:跨 Node 的全局数据约定。

核心源码剖析 (pocketflow/__init__.py)

PocketFlow的强大之处在于其简约的核心抽象。让我们深入其仅有100行的源码,逐一拆解其精妙设计。

BaseNode

​BaseNode​​是所有节点的基石,它定义了节点最核心的两个属性和两个方法:

*   ​​self.successors​​​: 一个字典,形态为​​{'action_name': next_node}​​​。这是PocketFlow流程控制的脉搏。​​post​​​方法返回的​​action​​字符串就是在这个字典里查找下一个要执行的节点。

*   ​​self.params​​: 另一个字典,用于接收外部传入的、节点级别的参数,在批处理场景(Batch)中尤其重要。

*   ​​next(self, node, actinotallow="default")​​​: 这个方法负责填充​​successors​​​字典。​​node_a.next(node_b, "some_action")​​​就相当于​​node_a.successors["some_action"] = node_b​​。

*   ​​_run(self, shared)​​​: 这是节点的生命周期方法,它严格按照​​prep -> _exec -> post​​​的顺序执行,并将​​prep​​​的结果传递给​​_exec​​​,再将两者的结果传递给​​post​​。

class BaseNode:
    def __init__(self): self.params,self.successors={},{}
    def set_params(self,params): self.params=params
    def next(self,node,actinotallow="default"):
        if action in self.successors: warnings.warn(f"Overwriting successor for action '{action}'")
        self.successors[action]=node; return node
    def prep(self,shared): pass
    def exec(self,prep_res): pass
    def post(self,shared,prep_res,exec_res): pass
    def _exec(self,prep_res): return self.exec(prep_res)
    def _run(self,shared): p=self.prep(shared); e=self._exec(p); return self.post(shared,p,e)
    def run(self,shared): 
        if self.successors: warnings.warn("Node won't run successors. Use Flow.")  
        return self._run(shared)

DSL: >> 和 - 语法糖

PocketFlow中最令人惊艳的莫过于其定义流程的方式,如 ​​node_a - "action" >> node_b​​。这其实是巧妙地利用了Python的魔法方法实现的:

*   ​​__sub__(self, action)​​​: 当我们写​​node_a - "action"​​​时,Python会调用​​node_a​​​的​​__sub__​​​方法。这个方法并不做减法,而是返回一个临时的​​_ConditionalTransition​​​对象,这个对象保存了​​node_a​​​和​​"action"​​。

*   ​​__rshift__(self, other)​​​: 当我们写​​... >> node_b​​​时,Python会调用​​__rshift__​​方法。

*   如果直接是​​node_a >> node_b​​​,​​node_a​​​的​​__rshift__​​​被调用,它等价于​​node_a.next(node_b, "default")​​。

*   如果是​​_ConditionalTransition(...) >> node_b​​​,那么临时对象的​​__rshift__​​​被调用,它执行的是​​source_node.next(node_b, "saved_action")​​。

class BaseNode:
    ......
    def __rshift__(self,other): return self.next(other)
    def __sub__(self,action):
        if isinstance(action,str): return _ConditionalTransition(self,action)
        raise TypeError("Action must be a string")

class _ConditionalTransition:
    def __init__(self,src,action): self.src,self.actinotallow=src,action
    def __rshift__(self,tgt): return self.src.next(tgt,self.action)

通过这短短几行代码,PocketFlow就创造出了一种极具表现力的领域特定语言(DSL),让流程定义变得像写诗一样自然。

Node

​Node​​​类在​​BaseNode​​的基础上,增加了至关重要的容错机制

class Node(BaseNode):
    def __init__(self,max_retries=1,wait=0): super().__init__(); self.max_retries,self.wait=max_retries,wait
    def exec_fallback(self,prep_res,exc): raise exc
    def _exec(self,prep_res):
        for self.cur_retry in range(self.max_retries):
            try: return self.exec(prep_res)
            except Exception as e:
                if self.cur_retry==self.max_retries-1: return self.exec_fallback(prep_res,e)
                if self.wait>0: time.sleep(self.wait)

这里的​​_exec​​​方法覆盖了​​BaseNode​​​的版本。它不再是简单地调用​​self.exec​​​,而是用一个​​for​​​循环包裹了​​try...except​​块。

*   循环: ​​max_retries​​参数决定了循环次数。

*   ​try​​: 尝试执行开发者定义的​​self.exec(prep_res)​​。如果成功,直接返回结果。

*   ​except​: 如果捕获到任何异常,它会检查是否是最后一次重试。

*   如果是,则调用​​exec_fallback​​方法,让开发者有机会进行优雅的失败处理(默认是直接抛出异常)。

*   如果不是,则根据​​wait​​参数等待一段时间后,进入下一次循环重试。

Flow

​Flow​​​是整个工作流的驱动引擎,其核心是​​_orch​​(orchestrate,编排)方法。

class Flow(BaseNode):
    def __init__(self,start=None): super().__init__(); self.start_node=start
    # ...
    def get_next_node(self,curr,action):
        nxt=curr.successors.get(action or"default")
        ifnot nxt and curr.successors: warnings.warn(f"Flow ends: '{action}' not found in {list(curr.successors)}")
        return nxt
    def _orch(self,shared,params=None):
        curr,p,last_action =copy.copy(self.start_node),(params or {**self.params}),None
        while curr:
            curr.set_params(p)
            last_actinotallow=curr._run(shared)
            curr=copy.copy(self.get_next_node(curr,last_action))
        return last_action

​_orch​​的逻辑非常清晰:

  1. 初始化:​​curr​​​指针指向​​start_node​​。
  2. while循环: 只要​​curr​​​不为​​None​​,循环就继续。
  3. 执行: 调用​​curr._run(shared)​​​来执行当前节点的​​prep->exec->post​​​生命周期,并将其​​post​​​方法的返回值存为​​last_action​​。
  4. 寻路: 调用​​get_next_node(curr, last_action)​​​,在当前节点的​​successors​​​字典中寻找​​last_action​​对应的新节点。
  5. 前进: 将​​curr​​​指针更新为找到的新节点。如果找不到,​​get_next_node​​​返回​​None​​,循环将在下一次检查时终止。
  6. ​copy.copy()​​的使用确保了每个节点的实例在flow的单次运行中是独立的,避免了状态污染。

在我看来,PocketFlow的源码展现了"少即是多"的原则。它没有试图成为一个包罗万象的巨型框架,而是专注于提供一套最核心、最灵活的构建块,将其他的一切(如工具调用、API封装)都交由开发者在节点内部自由实现。

总结

说实话,PocketFlow目前的易用性还有待提升,不如许多框架那样开箱即用。但正是这种精简设计赋予了它更大的灵活性,开发者可以根据自己的需求进行DIY(详细实现方案可参考官方Cookbook:https://github.com/The-Pocket/PocketFlow/tree/main/cookbook)


本文转载自AI 博物院 作者:longyunfeigu

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2025-7-2 06:00:54修改
收藏
回复
举报
回复
相关推荐