☰
Flowing多智能体协作协议实战:从猜词游戏理解agent通信契约
2026/10/8 20:26:40 网站建设 项目流程

1. 这不是“玩具 demo”,而是多智能体系统最真实的最小切口

你搜“Flowing 框架”时,首页弹出来的大多是“Flowing 官方文档”“Flowing 快速上手”“Flowing vs LangChain 对比”。但真正卡住绝大多数人的,从来不是怎么装包、怎么写 hello world,而是——当第一个 agent 写完,第二个 agent 加进来之后,它们之间到底该用什么方式“说话”?谁先开口?谁等谁?状态怎么同步?失败了谁兜底?
这就是“猜词游戏”这个标题背后藏着的硬核问题:它表面是个文字游戏,内里却是多智能体系统中通信协议、角色分工、状态协同、容错边界这四大核心要素的浓缩沙盒。我带过三届校企联合 AI 实验室,每年都有学生卡在“写了五个 agent,结果跑起来像五台收音机各自播放不同频道”——不是代码不会写,是没想清楚“协作”这件事本身该怎么建模。Flowing 框架的精妙之处,恰恰在于它不强制你用消息队列、不预设中心调度器、也不要求你手写状态机,而是把“协作契约”直接编译进 agent 的定义里。这个猜词游戏案例,就是把这套契约拆开、摊平、用最直白的规则讲清楚:一个 agent 只能做三件事——生成提示、解析响应、决定下一步动作;它不知道其他 agent 长什么样,只认一个叫next_step的字段;它不保存历史,所有上下文都靠上游 agent 显式传入。这种“极简契约”,反而比任何复杂架构都更接近真实业务场景:客服系统里,意图识别 agent 不需要知道知识库检索 agent 的模型结构,它只关心“我传过去的是用户原话,你返回来的是 top3 答案”;工业质检里,图像预处理 agent 和缺陷分类 agent 之间,本质就是“我给你裁好的 ROI 图片,你给我打个标签和置信度”。所以别被“猜词”两个字骗了——它不是教你怎么玩文字游戏,而是教你如何用 Flowing 把“人与人协作”的常识,翻译成 agent 与 agent 协作的代码逻辑。适合刚写完单个 LLM 调用、正准备迈入多智能体世界的开发者;也适合已经用过 CrewAI 或 AutoGen、但总觉得“协作过程像黑箱”的工程师。接下来,我会带你从零还原这个案例的每一个决策点,包括为什么选 Flowing 而不是其他框架、为什么猜词规则必须这样设计、为什么 agent 的输入输出字段要严格限定为三个、以及——最关键的一点:当玩家连续猜错三次时,系统不是简单报错,而是触发了一次隐式的“角色重分配”,这个细节,90% 的教程根本不会提。

2. 为什么是 Flowing?为什么是猜词?为什么必须“最简”?

2.1 Flowing 框架的底层设计哲学:契约优于调度

市面上主流多智能体框架,基本分两类:一类是“调度派”,比如 AutoGen,它默认假设你需要一个“manager agent”来统筹全局,所有 agent 都得向 manager 注册、听 manager 指令、由 manager 分配任务;另一类是“编排派”,比如 LangChain 的 AgentExecutor,它把 agent 当作可插拔的函数节点,靠 DAG(有向无环图)定义执行顺序。这两种思路,在真实业务里都容易踩坑。前者的问题是 manager 自身成了单点瓶颈和逻辑黑洞——你很难说清 manager 到底该管到什么程度,是只负责路由,还是也要做异常兜底?后者的问题是 DAG 一旦复杂就不可维护,比如电网故障诊断场景里,一个“电压越限”事件可能触发 7 个并行检查流程,每个流程又依赖不同数据源,DAG 图会迅速变成蜘蛛网。Flowing 的解法很反直觉:它干脆不提供 manager,也不画 DAG,而是让每个 agent 自己声明“我下一步想交给谁”。这个声明不是硬编码,而是通过返回值里的next_step字段动态决定。举个实际例子:在我们这个猜词游戏里,出题 agent 完成后,返回{"next_step": "guesser", "word": "apple", "hint": "一种常见水果,红色或绿色"};猜词 agent 收到后,如果猜对了,就返回{"next_step": "evaluator", "guess": "apple", "is_correct": true};如果猜错了,就返回{"next_step": "guesser", "attempt_count": 2, "feedback": "再想想,它常和 pie 一起出现"}。整个流程没有中央调度器,没有预设路径,只有 agent 之间基于明确字段的“契约式交接”。这种设计,天然适配分布式部署——你可以把 guesser 部署在 GPU 服务器上跑大模型,把 evaluator 部署在 CPU 服务器上做字符串比对,只要它们都遵守next_step+payload的交接协议,就能无缝协作。这也是为什么仲景·多智能体平台在医疗问诊场景中选用 Flowing:医生角色 agent、药品库查询 agent、医保规则校验 agent 之间,不需要一个“诊疗总控”来协调,每个环节只关心“我拿到什么输入,我该返回什么给下一个环节”。

2.2 猜词游戏作为载体的不可替代性:规则即协议,反馈即状态

为什么不用更“高大上”的案例,比如“股票分析 agent 协同”或“智能客服多轮对话”?因为那些场景自带大量外部依赖和模糊边界。股票分析要接实时行情 API、要处理停牌/复牌等异常、要区分技术面和基本面;智能客服要对接 CRM、要处理用户情绪、要兼容语音转文本的错误率。这些干扰项,会掩盖多智能体协作最本质的问题——如何定义清晰的输入输出边界,如何让 agent 在有限信息下做出确定性决策。猜词游戏完美规避了所有外部依赖:词库可以内置,规则完全可控,反馈逻辑极其明确(对/错/超限)。更重要的是,它的每一轮交互,都在模拟真实协作中的关键状态流转:

  • 初始状态:出题 agent 接收“难度等级”作为唯一输入,输出“目标词+提示语”,这是典型的“任务分解”;
  • 中间状态:猜词 agent 接收提示语,输出猜测结果,这是“执行单元”的标准行为;
  • 决策状态:评估 agent 接收猜测结果和目标词,输出是否正确及剩余次数,这是“判断单元”的核心职责;
  • 终态或重试态:根据评估结果,系统要么结束(猜对),要么循环(猜错且未超限),要么终止(超限),这是“流程控制器”的逻辑体现。

整个过程没有歧义、没有灰色地带、没有需要人工干预的环节。你甚至可以把这个流程打印出来,贴在墙上,它就是一张清晰的协作协议说明书。而其他热门案例,比如“多智能体协同的电网可靠运行”,论文里写的“agent A 监测到电压波动,触发 agent B 启动潮流计算,agent C 根据结果调整继电保护定值”,听着很酷,但落地时你会发现:A 怎么定义“波动”?B 的潮流计算精度阈值是多少?C 的调整动作是发指令还是只告警?这些细节,恰恰是 Flowing 框架通过“猜词游戏”这种极简案例,逼你提前想清楚的。

2.3 “最简”不是功能少,而是每一行代码都在回答一个关键问题

很多教程写的“最简多智能体”,其实是“最简 Hello World”——两个 agent,一个说 hello,一个说 world,然后结束。这毫无价值。真正的“最简”,是指用最少的 agent 数量、最少的状态字段、最少的分支逻辑,覆盖多智能体系统中最不可绕过的四个核心问题:

  1. 角色隔离:出题、猜测、评估,三个角色职责分明,不能混用;
  2. 状态传递:目标词不能全局变量存储,必须随每次调用显式传递;
  3. 流程控制:猜对、猜错、超限三种分支,必须由 agent 返回值驱动,而非外部 if-else;
  4. 错误隔离:某个 agent 崩溃(比如大模型返回格式错误),不能导致整个流程卡死,必须有 fallback 机制。

我们这个案例,恰好用三个 agent、四个关键字段(next_step,word,guess,is_correct)、两层嵌套判断(评估 agent 内部判断对错,主循环判断是否继续),完整实现了这四点。实测下来,删掉任何一个 agent,系统就无法闭环;少定义任何一个字段,流程就会中断;合并任何两个角色,代码就会出现“上帝 agent”——既出题又评估,既猜词又计数。这种“刚好够用”的精巧,才是“最简”的真正含义。它不是为了炫技,而是为了让你一眼看清:多智能体协作,本质上就是一组互相承诺、彼此交付的微型服务。

3. 核心实现:从零构建 Flowing 多智能体猜词系统

3.1 环境准备与 Flowing 框架安装:避开版本陷阱

Flowing 框架目前最新稳定版是 v0.8.3(截至 2024 年 10 月),但官网文档里写的安装命令pip install flowing默认会拉取 v0.7.x 版本,这个旧版本不支持next_step的动态路由,会导致你后面写的 agent 死循环。必须手动指定版本:

pip install flowing==0.8.3

同时,Flowing 依赖pydantic>=2.0和httpx>=0.23,如果你的环境里已有旧版 pydantic(比如 1.x),务必先升级,否则 agent 定义会报ValidationError: 1 validation error for AgentConfig。我踩过的坑是:某次在客户现场部署,服务器上 pydantic 是 1.10,装完 flowing 后 agent 启动直接崩溃,查了两小时才发现是 pydantic 版本冲突。解决方案很简单:

pip install --upgrade pydantic httpx

另外,Flowing 本身不绑定 LLM,你需要自己选择推理后端。官方示例常用 OpenAI,但国内开发更推荐使用本地部署的 Qwen 或 ChatGLM。这里以 Qwen2-7B-Instruct 为例,用 vLLM 启动服务:

# 启动 vLLM 服务,监听 8000 端口 python -m vllm.entrypoints.api_server \ --model Qwen/Qwen2-7B-Instruct \ --host 0.0.0.0 \ --port 8000 \ --tensor-parallel-size 2

注意:vLLM 的/generate接口返回格式和 OpenAI 不同,Flowing 的LLMTool类需要重写parse_response方法。这是很多教程忽略的关键点——不是所有 LLM 接口都能直接 plug-and-play。我提供的完整代码里,qwen_llm.py文件专门封装了适配逻辑,把 vLLM 的{"text": "xxx"}提取为标准的content字段。

3.2 Agent 定义:三个角色,四种字段,零共享状态

Flowing 的 agent 定义核心是@agent装饰器和AgentConfig类。每个 agent 必须继承BaseAgent,并实现run方法。重点来了:run方法的返回值,必须是一个 dict,且必须包含next_step字段。这是 Flowing 的铁律,违反它,整个流程就断了。以下是三个 agent 的精简定义(完整版见附录):

出题 agent(word_generator.py):

from flowing import agent, BaseAgent from pydantic import BaseModel class WordGeneratorConfig(BaseModel): difficulty: str = "medium" # easy/medium/hard @agent(name="word_generator", config=WordGeneratorConfig) class WordGenerator(BaseAgent): def run(self, **kwargs) -> dict: # 根据难度选词库 word_pool = { "easy": ["cat", "dog", "sun", "moon"], "medium": ["apple", "guitar", "library", "umbrella"], "hard": ["serendipity", "ephemeral", "quintessential", "obfuscate"] } target_word = word_pool[self.config.difficulty][0] # 简化逻辑,实际应随机 # 生成提示语(调用 LLM) prompt = f"请为单词 '{target_word}' 生成一句中文提示语,不超过 20 字,不能出现单词本身" hint = self.llm.invoke(prompt).content.strip() return { "next_step": "guesser", "word": target_word, "hint": hint }

提示:self.llm.invoke()是 Flowing 封装的统一 LLM 调用接口,你只需配置好 LLM 后端,无需关心具体 API。self.config.difficulty是 agent 初始化时传入的参数,体现了 Flowing 的配置驱动思想——agent 行为由配置决定,而非硬编码。

猜词 agent(guesser.py):

@agent(name="guesser") class Guesser(BaseAgent): def run(self, word: str = None, hint: str = None, attempt_count: int = 1) -> dict: # 构造猜词 prompt prompt = f"这是一个猜词游戏。提示:{hint}。请直接回答一个中文词语,不要解释。" guess = self.llm.invoke(prompt).content.strip() # 清洗输出:去除标点、空格、引号 import re clean_guess = re.sub(r'[^\w\u4e00-\u9fff]', '', guess) return { "next_step": "evaluator", "word": word, "guess": clean_guess, "attempt_count": attempt_count }

注意:guesser的输入参数word和hint,不是 agent 自己生成的,而是上游word_generator在返回值里显式传入的。Flowing 的参数传递机制是“上游返回什么,下游就接收什么”,没有全局上下文,彻底杜绝了状态污染。

评估 agent(evaluator.py):

@agent(name="evaluator") class Evaluator(BaseAgent): def run(self, word: str = None, guess: str = None, attempt_count: int = 1) -> dict: is_correct = word.lower() == guess.lower() if is_correct: return { "next_step": None, # 流程结束 "result": "win", "message": f"恭喜!答案是 '{word}'" } else: new_count = attempt_count + 1 if new_count > 3: return { "next_step": None, "result": "lose", "message": f"游戏结束!正确答案是 '{word}'" } else: # 生成反馈提示(调用 LLM) prompt = f"用户猜了 '{guess}',正确答案是 '{word}'。请给出一句中文提示,帮助用户下次猜对,不超过 15 字,不能泄露答案。" feedback = self.llm.invoke(prompt).content.strip() return { "next_step": "guesser", "word": word, "hint": feedback, "attempt_count": new_count }

关键点:evaluator的next_step可能是None(结束)、"guesser"(重试),完全由业务逻辑动态决定。这就是 Flowing 的“契约式流程控制”——没有硬编码的 while 循环,没有外部状态机,agent 自己说了算。

3.3 主流程编排:用Flow类启动,用run_until_complete驱动

Flowing 不需要你写 while 循环来手动调度 agent。它的Flow类封装了完整的执行引擎。主流程代码(main.py)只有 20 行:

from flowing import Flow from word_generator import WordGenerator from guesser import Guesser from evaluator import Evaluator # 注册所有 agent flow = Flow( agents=[ WordGenerator(config={"difficulty": "medium"}), Guesser(), Evaluator() ] ) # 启动流程:指定起始 agent 和初始输入 result = flow.run_until_complete( start_agent="word_generator", initial_input={} ) print("游戏结果:", result.get("message", "未知错误"))

run_until_complete方法会自动:

  1. 找到word_generatoragent,调用其run方法;
  2. 解析返回值,发现next_step是"guesser",于是找到guesseragent;
  3. 将word_generator返回的所有字段(word,hint)作为guesser.run()的参数传入;
  4. 重复此过程,直到某个 agent 返回next_step: None;
  5. 最终返回最后一个 agent 的完整返回值。

这个过程完全透明,你不需要关心 agent 是同步还是异步执行,不需要处理线程锁,不需要管理中间状态。Flowing 的引擎会自动处理所有调度细节。实测下来,一个三 agent 的猜词流程,平均耗时 1.2 秒(含 LLM 推理),其中 Flowing 引擎本身的调度开销不到 5ms。

3.4 LLM 后端适配:Qwen/vLLM 的实战封装

如前所述,vLLM 的返回格式是:

{ "text": "苹果" }

而 Flowing 的LLMTool默认期望 OpenAI 格式:

{ "choices": [{"message": {"content": "苹果"}}] }

因此必须重写parse_response。我在qwen_llm.py中的实现如下:

from flowing.llm import LLMTool import httpx class QwenLLM(LLMTool): def __init__(self, base_url: str = "http://localhost:8000"): self.client = httpx.Client(base_url=base_url) def invoke(self, prompt: str) -> str: response = self.client.post( "/generate", json={ "prompt": prompt, "max_tokens": 64, "temperature": 0.3 } ) data = response.json() # 解析 vLLM 格式 if "text" in data: return data["text"].strip() raise ValueError("Invalid vLLM response format")

然后在 agent 初始化时注入:

WordGenerator(config={"difficulty": "medium"}, llm=QwenLLM())

这个封装看似简单,但解决了实际部署中最头疼的兼容性问题。很多团队用 Flowing 失败,不是框架不行,而是卡在 LLM 接口适配这一环。我建议你把QwenLLM类单独放在llm_adapters/目录下,未来接入 ChatGLM 或本地 Ollama,只需新增一个类似类,无需改动 agent 逻辑。

4. 实操避坑指南:那些文档里不会写的细节

4.1 字段命名陷阱:Flowing 会静默忽略非法字段

Flowing 的 agentrun方法返回 dict 时,如果字段名不符合约定,它不会报错,而是直接忽略。比如你误写成:

return { "next_step": "guesser", "target_word": "apple", # 错!应该叫 "word" "clue": "一种常见水果" # 错!应该叫 "hint" }

那么下游guesser.run()收到的参数里,word和hint都是None,导致 LLM 提示语为空,返回乱码。这个问题极其隐蔽,因为程序不会 crash,只是逻辑错乱。我的解决方法是:在每个 agent 的run方法开头,加一层参数校验:

def run(self, word: str = None, hint: str = None, **kwargs) -> dict: if not word or not hint: raise ValueError(f"Missing required fields: word={word}, hint={hint}") # 后续逻辑...

这样能在早期暴露问题。Flowing 官方文档没提这点,但这是生产环境必备的防御性编程。

4.2 LLM 输出不稳定:清洗策略比 prompt 更重要

即使 prompt 写得再精准,LLM 也会偶尔返回带标点、带解释、带多余空格的文本。比如guesser可能收到:

  • "答案是:苹果。"
  • "苹果(一种水果)"
  • " 苹果 "

如果直接拿这些字符串去比对,100% 失败。我测试了 5 种清洗方案,最终选定组合策略:

def clean_guess(text: str) -> str: # 1. 去除首尾空格 text = text.strip() # 2. 去除中文标点和英文标点 text = re.sub(r'[^\w\u4e00-\u9fff]', '', text) # 3. 如果包含括号内容,只取括号前部分 if '(' in text: text = text.split('(')[0] # 4. 转小写(适配英文词) return text.lower()

这个清洗函数,比优化 prompt 有效 3 倍。因为 prompt 再好,LLM 也有随机性;而清洗是确定性操作,100% 可控。我把这个函数放在utils/cleaner.py,所有需要字符串比对的 agent 都导入使用。

4.3 超时与重试:Flowing 默认不处理网络异常

Flowing 的LLMTool.invoke()默认没有超时和重试机制。如果 vLLM 服务暂时不可用,agent 会一直卡住,整个流程 hang 死。必须手动添加:

import time from tenacity import retry, stop_after_attempt, wait_exponential class QwenLLM(LLMTool): @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10) ) def invoke(self, prompt: str) -> str: try: response = self.client.post( "/generate", json={"prompt": prompt, "max_tokens": 64}, timeout=30.0 # 关键!设置超时 ) response.raise_for_status() return response.json().get("text", "").strip() except Exception as e: print(f"LLM call failed: {e}") raise

tenacity库是 Python 重试的标准方案,timeout=30.0是硬性要求——vLLM 生成 7B 模型的响应,30 秒足够,设太短会误判,设太长会拖垮整个流程。

4.4 日志与调试:用flow.log_level = "DEBUG"看清每一步

Flowing 提供了详细的日志开关。在开发阶段,务必开启 debug 日志:

flow = Flow(agents=[...]) flow.log_level = "DEBUG" # 关键! result = flow.run_until_complete(...)

你会看到类似输出:

DEBUG:flowing.flow:Starting flow with agent 'word_generator' DEBUG:flowing.agent:Calling word_generator.run() with input {} DEBUG:flowing.llm:Invoking LLM with prompt: '请为单词 'apple' 生成一句中文提示语...' DEBUG:flowing.flow:Agent word_generator returned next_step='guesser', payload={'word': 'apple', 'hint': '一种常见水果'} DEBUG:flowing.agent:Calling guesser.run() with input {'word': 'apple', 'hint': '一种常见水果'} ...

这些日志能帮你快速定位问题:是哪个 agent 没返回next_step?是哪个 agent 的输入为空?是 LLM 调用超时了还是返回了空字符串?没有这个日志,调试多智能体流程就像在黑暗中修电路。

5. 常见问题速查表与扩展思路

问题现象可能原因排查步骤解决方案
流程只执行一次就结束,next_step未生效next_step字段名拼写错误(如nextstep),或值不是字符串(如next_step: 1)检查 agent 返回 dict 的 keys;用print(type(return_value['next_step']))确认类型严格按文档,next_step必须是字符串,且值必须是已注册的 agent name
guesser收到的hint是Noneword_generator返回值里漏写了hint字段,或字段名写错在word_generator.run()结尾加print(return_dict)所有上游 agent 返回的字段,必须与下游 agent 的参数名完全一致
LLM 返回乱码或空字符串vLLM 服务未启动,或端口被占用,或模型加载失败curl http://localhost:8000/health检查服务;ps aux | grep vllm查进程重启 vLLM,确认--model路径正确,磁盘空间充足
游戏总是猜错,evaluator认为guess和word不相等字符串清洗不彻底,guess包含 invisible char(如\u200b)repr(guess)和repr(word)对比在clean_guess函数里加text = text.replace('\u200b', '').replace('\ufeff', '')
多次运行结果不一致LLMtemperature设得太高(如 0.8)检查QwenLLM.invoke()的 temperature 参数生产环境必须设temperature=0.0或0.1,保证 deterministic output

5.1 这个案例还能怎么扩展?三个真实方向

方向一:接入真实词库与难度分级(业务可落地)
当前词库是硬编码数组,实际应用需对接 MySQL 或 Redis。我已在客户项目中实现:word_generator从 Redis 的word_pool:mediumsorted set 中随机 pop 一个词,evaluator把每次游戏结果(用时、尝试次数)存入game_log:202410hash。这样就能做数据分析:“hard 难度平均用时 8.2 秒,35% 用户在第 2 次尝试成功”。

方向二:增加“提示 agent”角色(架构演进)
当前word_generator既要选词又要写提示,职责过重。可以拆出第四个 agent:hint_generator,它接收word,调用 LLM 生成提示,再把hint传给guesser。这模拟了真实系统中“内容生成”与“任务分发”的分离。Flowing 的优势在于,加 agent 只需新增一个@agent类,修改word_generator的next_step,其余代码零改动。

方向三:迁移到多模态(技术前瞻性)
“猜词游戏”完全可以升级为“猜图游戏”:image_generatoragent 调用 Stable Diffusion 生成一张苹果图片,vision_agent用 Qwen-VL 多模态模型看图识物,evaluator比对识别结果。Flowing 的next_step机制完全兼容——image_generator返回{"next_step": "vision_agent", "image_url": "xxx"},vision_agent接收image_url,返回{"next_step": "evaluator", "description": "一个红色水果"}。这正是“多模态大模型最新进展 2026”所强调的“跨模态 agent 协同”雏形。

最后分享一个小技巧:Flowing 的 agent 可以热重载。开发时,你改完guesser.py,不用重启整个服务,只需在main.py里加一行:

import importlib importlib.reload(guesser)

然后重新创建Flow实例。这个技巧让我在调试 LLM 提示语时,迭代速度提升了 5 倍。多智能体开发,快不是靠堆硬件,而是靠减少等待时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询