惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hacker News: Ask HN
Hacker News: Ask HN
WordPress大学
WordPress大学
T
The Blog of Author Tim Ferriss
The GitHub Blog
The GitHub Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
A
About on SuperTechFans
Stack Overflow Blog
Stack Overflow Blog
雷峰网
雷峰网
Microsoft Azure Blog
Microsoft Azure Blog
腾讯CDC
爱范儿
爱范儿
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 【当耐特】
V
Visual Studio Blog
有赞技术团队
有赞技术团队
U
Unit 42
D
Docker
小众软件
小众软件
F
Full Disclosure
I
Intezer
Scott Helme
Scott Helme
P
Privacy International News Feed
P
Proofpoint News Feed
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
B
Blog
Martin Fowler
Martin Fowler
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Vercel News
Vercel News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Spread Privacy
Spread Privacy
宝玉的分享
宝玉的分享
S
Security Affairs
www.infosecurity-magazine.com
www.infosecurity-magazine.com
月光博客
月光博客
C
Cisco Blogs
云风的 BLOG
云风的 BLOG
Schneier on Security
Schneier on Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Threat Research - Cisco Blogs
量子位
Hacker News - Newest:
Hacker News - Newest: "LLM"
H
Heimdal Security Blog
N
Netflix TechBlog - Medium
H
Hacker News: Front Page
P
Proofpoint News Feed
G
GRAHAM CLULEY
V
Vulnerabilities – Threatpost
S
Schneier on Security

阿尔的代码屋 | 全栈技术笔记

在 Android Termux 环境下安装 Hermes Agent 的踩坑与完美解决实践 开发日志| 阿尔的代码屋 VS Code 连接 WSL 精确每 10 分钟掉线 排坑笔记 | 阿尔的代码屋 Android 模拟器代理联网与 No Internet WiFi 锁死排坑笔记 | 阿尔的代码屋 [object Object] typing_extensions 有用(四):使用 TypeIs 替代危险的 cast,做最严谨的类型收窄 | 阿尔的代码屋 GoRouter 结合 Isar 运行 Widget 测试并发/粘性线程死锁卡死排坑笔记 | 阿尔的代码屋 typing_extensions 有用(三):使用 Unpack 结合 TypedDict 给 **kwargs 装上透视眼 | 阿尔的代码屋 typing_extensions 有用(二):使用 @override 打造重构代码时的“防呆神器” | 阿尔的代码屋 Flutter 并发测试踩坑实录 - IsarCore 动态库下载冲突与 Widget 测试 HTTP 拦截全链路解决 | 阿尔的代码屋 typing_extensions 有用(一):使用 Self 终结继承时的类型推断灾难 | 阿尔的代码屋 基于 Cloudflare Pages 的纯前端 WebAssembly 应用自动化部署实践 | 开发日志 | 阿尔的代码屋 基于 VS Code 远程开发的 GPU Docker 容器自动清理方案实践 开发日志| 阿尔的代码屋 Patrol iOS 集成测试排坑实录 - xcodebuild exit code 70 全链路解决 | 阿尔的代码屋 Flutter E2E 测试从 integration_test 迁移到 Patrol - 实践笔记 | 阿尔的代码屋 Linux/macOS 下 micromamba 报错 Shard Index not available 与极度卡顿 排坑笔记 | 阿尔的代码屋 critical libmamba Shell not initialized micromamba报错 subprocess 无法修改父 Shell - 排坑笔记 | 阿尔的代码屋 VS Code Remote-SSH: 彻底告别重复输入密码 - 排坑笔记 | 阿尔的代码屋 Git 报警: inexact rename detection was skipped - 排坑笔记 | 阿尔的代码屋 解决群晖 NAS SSL 证书自动部署难题 | 阿尔的代码屋 Windows 下 Git error: fatal: refusing to work with credential missing host field - 排坑笔记 | 阿尔的代码屋 VS Code 调试 Python 第三方库源码的配置 - 开发技巧| 阿尔的代码屋 VS Code Remote Tunnels本地连接调试Kaggle GPU - 开发技巧| 阿尔的代码屋 Windows下Git error: open(xxxx) Filename too long - 排坑笔记 | 阿尔的代码屋 记录2026年零预约港卡开户实录避坑 告别孤岛:拥抱 MCP 协议,为大模型打造标准“USB 接口” - 大模型实战 08 完结篇 | 阿尔的代码屋 基于 LlamaIndex ReAct 框架手搓全自动博客监控 Agent - 大模型实战 07 | 阿尔的代码屋 在 Kaggle 上用 Unsloth 极速微调 Qwen3 - 大模型实战 06 | 阿尔的代码屋 大模型微调实战详解 - 大模型实战 05 | 阿尔的代码屋 基于 ChromaDB 打造工程级 RAG 系统 - 大模型实战 04 | 阿尔的代码屋 拆解 Transformers 原理与图解 - 大模型实战 03 | 阿尔的代码屋 云端炼丹房 2:Kaggle 上手指南 - 大模型实战 03预备 | 阿尔的代码屋 云端炼丹房:Google Colab 免费 GPU 上手指南 - 大模型实战 03预备 | 阿尔的代码屋 Open WebUI 部署与 RAG 实战 - 大模型实战 02 | 阿尔的代码屋
从 ReAct 到 Workflow:基于云端 API 构建事件驱动的智能体 - 大模型实战 07 额外篇 | 阿尔的代码屋
Algieba · 2026-03-03 · via 阿尔的代码屋 | 全栈技术笔记

核心摘要 (TL;DR)

  • 架构进阶:从传统的 ReAct 循环升级为 LlamaIndex Workflow,掌握基于“事件驱动”的现代智能体编排范式。
  • 算力自由:摆脱本地 GPU 显存与运行时间的限制,实战接入 Groq 与硅基流动的高性能免费大模型 API。
  • 实战目标:利用 Workflow 重构博客监控程序,将其拆解为“检查员 (Checker)”与“通知官 (Notifier)”,实现多智能体的解耦与协作。

前言

上一篇博客,我们用本地加载的huggingface模型来定义的ReActAgent完成了任务。其实对于LlamaIndex而言,更规范的用法是用WorkFlow来定义Agent,用API来调用而不是用本地加载模型,所以本篇博客的主要内容就是介绍一下LlamaIndex的workflow流程。

之前咱们的用法是一种QueryEngine的用法,就是将大模型当成一个查询的工具在使用,而workflow是LlmaIndex的新一代编排引擎

1.1 核心逻辑

LlamaIndex的workflow,本质上是一个事件驱动(Event-driven)的状态机,如果各位友人们经常做开发的话,一定一下就理解。 咱们可以将其流程想象成一场"接力赛":

  • 选手(Steps): 我们会在代码里用@step装饰函数,这个函数就是step,负责具体的逻辑。
  • 接力棒(Events):我们会用来判断状态的自定义类,继承自Event,用于在Step间传递数据。
  • 终点线(StopEvent): 和普通的Event一样,只是作为最后一棒,某个Step返回StopEvent就意味着"接力赛"结束了,咱们将结果返回给用户。

1.2 核心组件

  1. Event
    是Workflow的灵魂,咱们可以定义各种Event,其决定了数据流向何处。
1
2
3
4
5
from llama_index.core.workflow import Event

class NewsExtractedEvent(Event):
content: str
xxx: int
  1. @step
    是逻辑处理单元。一个Step可以通过返回值来“发射”事件。

    • 生产者: 返回一个Event实例。
    • 消费者: 在接收的下一步Step函数的函数声明参数中加上对应的Event实例。
  2. StartEvent & StopEvent
    是流程的"起跑线"和"终点线"。一般我们只用定义StopEvent,当调用workflow.run()自动会触发StartEvent,任何Step只要返回了StopEvent,整个workflow就会停止并输出结果。

结合起来,就是这样一个极简的Workflow模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from llama_index.core.workflow import Workflow, step, StartEvent, StopEvent, Event

class ProcessedDataEvent(Event):
data: str

class MyWorkflow(Workflow):
@step
async def ingest(self, ev: StartEvent) -> ProcessedDataEvent:

input_data = ev.get("input")
return ProcessedDataEvent(data=input_data.upper())

@step
async def finish(self, ev: ProcessedDataEvent) -> StopEvent:

return StopEvent(result=f"处理完成: {ev.data}")


wf = MyWorkflow()
await wf.run(input="hello world")

通常来讲,咱们会把step定义给一个调用llm处理的函数. 我们待会儿就用Workflow重写咱们上一篇中的Agent。

2. API获取

咱们这次给大家换换口味,不用本地加载模型,而是用API的方式去调用大模型,所以本篇博客的代码可以在没有显卡的电脑上运行,可以脱离Kaggle的显卡环境。
PS: 虽然调用API更方便,但是本地加载或者本地部署更安全
(阿尔周末也尝试过在kaggle上直接去用vllm/ollama部署,然后调用,但是遇到了太多没法解决的问题,本次就直接用现成的API了。如果能解决,到时候再出一篇教程吧,如果还记得这个事儿的话,哈哈哈)
接下来介绍两个可以用免费模型的API: Groq硅基流动

2.1 Groq

Groq并不是模型开发者,只是大模型的搬运工,是一个推理加速平台,其核心是自研的LPU(Language Processing Unit),所以它的推理速度那是相当惊人,这也是阿尔推荐它的很重要的一个理由。

  • Groq模型矩阵
模型系列模型 ID (API 调用名)厂商上下文窗口特点与优势
Llama 3.3llama-3.3-70b-versatileMeta128k最强逻辑,性能对标 GPT-4o
Llama 3.1llama-3.1-8b-instantMeta128k极速响应,适合简单逻辑与翻译
Llama 3.2llama-3.2-11b-vision-previewMeta128k多模态支持,可识别并理解图片
Gemma 2gemma2-9b-itGoogle8k指令遵循强,结构化输出更稳定
Mixtralmixtral-8x7b-32768Mistral32k经典的混合专家模型,长文本表现好
Whisperwhisper-large-v3OpenAI-极速语音转文字 (STT)
  • Groq的限制
    Groq的免费层级给的非常慷慨,堪称大善人,但是为了防止滥用,还是设置了很多限制。通常对每个模型独立计算
    通常从RPM(Requets Per Minute)每分钟请求数,RPD(Requests Per Day)每天请求数,TPM(Tokens Per Minute)每分钟消耗Token数三个维度限制,当然对于咱们来说是完全够用的

  • 注册Groq Api Key

    • 进入Groq官网,点击右上角的Start Building
    • 用谷歌账号登录即可
    • 点击最上面菜单栏的API KEYS
    • 点击 Create API Key,可以设置一个过期日期,然后保存下来,待会儿咱们配置到kaggle的Secrets中即可
  • 生成API Key

2.2 硅基流动

硅基流动是咱们要介绍的二号大模型的搬运工,国内开发者使用比较多,可以在上面找到很多便宜大碗的模型,也有不少免费的模型可以用,咱们今天就要用它的免费模型。

  • 硅基流动模型矩阵
模型系列最新代表型号 (API 调用名)核心优势上下文窗口状态
DeepSeekdeepseek-ai/DeepSeek-V3.2性能对标顶级闭源模型,极强逻辑128k旗舰收费
DeepSeekdeepseek-ai/DeepSeek-R1推理增强模型,擅长复杂数学与逻辑64k部分免费
QwenQwen/Qwen3-30B-A3B阿里最新一代模型,中文语境指令遵循极强128k免费/收费
Qwen-VLQwen/Qwen3-VL-72B顶级视觉理解能力,支持视频分析32k旗舰收费
GLMZ.ai/GLM-5智谱旗舰,专注于长周期 Agent 任务205k旗舰收费
Kimimoonshotai/Kimi-K2.5原生多模态智能体模型,长文本处理极佳262k旗舰收费
Stepstepfun-ai/Step-3.5-Flash极高性价比的轻量模型,推理速度极快262k低成本/免费
MiniMaxMiniMaxAI/MiniMax-M2.5擅长角色扮演与情感交互197k旗舰收费
Llama 3meta-llama/Llama-3.3-70B-Instruct国际最强开源模型,适配各类 Agent 框架128k旗舰收费
  • 硅基流动的限制
    硅基流动有着比Groq更多的可用模型,有不少永久免费的模型, 甚至还有推广(阿尔这里就不贴推广码了,大家自行注册即可)和新用户赠金,但是其免费版的并发数很低,具体的模型也有RPM和TPM限制。

  • 注册硅基流动API Key

我们待会儿使用Qwen/Qwen3-8B这个免费模型

3. 代码实战

3.1 环境准备

这次和此前不同,诶,咱们今天用API不用自己加载模型,不用开GPU加速,Accelerator选择None即可。

然后老样子,先装依赖库

1
2
3
4
5
6
7
8

!pip install -U uv





!uv pip install llama-index llama-index-llms-groq llama-index-llms-openai-like feedparser requests transformers

注意,这里咱们硅基流动其实也是有专门的定制库的,llama-index-llms-siliconflow,只是这里咱们为了通用性用了llama-index-llms-openai-like。(实际上是因为最开始试了Groq效果不好,觉得换中文模型会好一点,然鹅效果也不好,这次主要还是介绍这个WorkFlow架构吧)

3.2 配置密钥

上篇博客一样,咱们在Add-ons->Secrets中添加上Groq硅基流动的API Key,命名为GROQ_API_KEYSILICON-API_KEY(当然,名字无所谓,和代码匹配就好)

3.3 使用API来示例llm

Tools和之前一样,咱们就不赘述了。

  • 定义Groq的模型
1
2
3
4
5
6
7
8
import os
from llama_index.llms.groq import Groq
from llama_index.core import Settings

os.environ["GROQ_API_KEY"] = user_secrets.get_secret("GROQ_API_KEY")


Settings.llm = Groq(model="llama-3.3-70b-versatile")
  • 定义硅基流动的Qwen模型
1
2
3
4
5
6
7
8
from llama_index.llms.openai_like import OpenAILike

Settings.llm = OpenAILike(
model="Qwen/Qwen3-8B",
api_key=user_secrets.get_secret("SILICON-API_KEY"),
api_base="https://api.siliconflow.cn/v1",
is_chat_model=True
)

可以看出来,定制的接口只用定义对应的API KEY在环境变量里,通用的接口需要补充清楚模型名,api-key,api的url等等。

3.4 整体流程

我们打算定义两个Agent

  • Checker: 负责去调用获取最新博客以及解析
  • Notifier: 负责向我们发送通知

当有新的博客的时候,Checker向Notifer发送事件,告知notifier新博客的标题和链接。然后结束。

整体流程如下:

3.5 定义Event

1
2
3
4
5
from llama_index.core.workflow import Workflow, step, StartEvent, StopEvent, Context, Event

class NewBlogPostEvent(Event):
title: str
link: str

由于Checker流转向Notifer的时候,只需要给它标题和链接即可,咱们的NewBlogPostEvent只用包含titlelink字段。

3.6 定义两个合作的Agent

我们先定义Checker

  • 交代好它的角色(ROLE)任务(TASK)输出要求(OUTPUT_FORMAT)
  • 将checker需要调用的工具给它
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from llama_index.core.agent.workflow import ReActAgent

CHECKER_SYSTEM_PROMPT = """
### ROLE
咱们是一个极其精准的数据提取专家。

### TASK
1. 调用工具获取最新博客文章。
2. 提取其标题和链接。
3. **必须**以 JSON 格式输出。

### OUTPUT_FORMAT
{"title": "文章标题", "link": "文章链接"}
"""
checker = ReActAgent(
name="checker",
system_prompt=CHECKER_SYSTEM_PROMPT,
tools=[tool_get_blog],
llm=Settings.llm
)

然后咱们再来定义Notifier

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NOTIFIER_SYSTEM_PROMPT = """
### ROLE
咱们是一个通知官。

### TASK
请根据提供的信息发送通知。
直接调用工具发送,不要解释咱们的行为。发送成功后回复“已发出”即可。
"""
notifier = ReActAgent(
name="notifier",
system_prompt=NOTIFIER_SYSTEM_PROMPT,
tools=[tool_send_all],
llm=Settings.llm
)

3.7 定义Workflow

  • Workflow类派生出我们的workflow类
  • 定义step,从StartEvent开始到StopEvent结束

这里因为模型的指令跟随能力比较差,格式化输出不了,咱们用了一个正则来兜底。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import os
import re
import json

global_last_title = ""



class DemoBlogMonitor(Workflow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.checker = checker

self.notifier = notifier

@step
async def check_step(self, ev: StartEvent) -> NewBlogPostEvent | StopEvent:
global global_last_title
rss_url = ev.get("rss_url")

print(f"--- [Checker] 目标: {rss_url} ---")
print(f"--- [Checker] 基准: '{global_last_title}' ---")

response = await self.checker.run(user_msg=f"请提取此 RSS 的最新数据:{rss_url}")
res_text = str(response)
print(f"DEBUG [Agent Output]: {res_text}")

try:
json_match = re.search(r"(\{.*\})", res_text, re.DOTALL)
if not json_match:
raise ValueError("未在回复中找到 JSON 块")

data = json.loads(json_match.group(1))
current_title = data.get("title", "").strip()
current_link = data.get("link", "").strip()

if not current_title or not current_link:
raise ValueError("JSON 数据缺失字段")


if current_title != global_last_title:
print(f"发现新动态: {current_title}")

return NewBlogPostEvent(title=current_title, link=current_link)
else:
return StopEvent(result=f"判定:内容未更新")

except Exception as e:

print(f"WARN: JSON 解析失败 ({e}), 尝试正则兜底...")
link_match = re.search(r"(https?://\S+)", res_text)
if link_match:
link = link_match.group(1).strip(" ”\"。")

title_match = re.search(r"[是“\"'](.*?)[”\"']", res_text)
title = title_match.group(1) if title_match else "未识别标题"

if title != global_last_title:
return NewBlogPostEvent(title=title, link=link)

return StopEvent(result=f"解析彻底失败。原文: {res_text}")

@step
async def notify_step(self, ev: NewBlogPostEvent) -> StopEvent:
global global_last_title
print(f"--- [Notifier] 正在发送: {ev.title} ---")

await self.notifier.run(user_msg=f"发送通知:标题《{ev.title}》,链接 {ev.link}")


global_last_title = ev.title
return StopEvent(result=f"完成。新基准已设为: {ev.title}")

3.8 运行

1
2
3
4
5
6
7
8
9
async def main():
monitor = DemoBlogMonitor(timeout=120)

print("\n>>> Run #1")
await monitor.run(rss_url="https://blog.algieba12.cn/atom.xml")

print("\n>>> Run #2")
await monitor.run(rss_url="https://blog.algieba12.cn/atom.xml")
await main()

这里咱们运行两遍,确认全局变量会同步更新到新状态,咱们实际使用的时候,可以将其持久化存储下来,而不是用变量global_last_title将状态保存在内存中

4. 代码

本篇博客代码示例可在Kaggle笔记本

5. 常见问题 (Q&A)

Q1: 为什么不让 Agent 直接判断是否有更新,而是要在 Python 代码里写 if current_title != global_last_title:
A: 为了系统的绝对稳定性!(划重点)
大模型在 ReAct 模式下有时会产生“幻觉”或者表现得过于“客气”。

  • 现象:最开始的设计是让模型自己比对已知标题和新标题,结果模型看到传进去的旧标题占位符,误以为这是一道“填空题”,不仅没有进行比对,还很有礼貌地让我把真实标题填进去。
  • 结论:最佳实践是逻辑解耦。让 Agent 专注于它最擅长的事情(从杂乱网页中精准提取非结构化数据),而将确定性的逻辑判断(字符串比对)交还给传统的 Python 代码来做。

Q2: 为什么一定要强制模型输出 JSON 格式?之前用 | 这种自定义分隔符不行吗?
A: 因为自然语言的边界太模糊,且极易与真实数据冲突。
这是本次开发多智能体协作时遇到的最大痛点!

  • 表达欲失控:高级模型(比如 Llama-3.3 或 Qwen)即使在 Prompt 里千叮咛万嘱咐只输出数据,也极大概率会在结果前后加上“好的,最新文章是…”之类的废话,导致 split("|") 直接报错。
  • 特殊符号冲突:碰巧咱们博客的标题里本身就带有 | 符号。正则匹配遇到这种情况会产生严重的边界歧义。
  • JSON 的优势:JSON 拥有明确的边界标识(花括号和双引号)。再配合 Python 正则 re.search(r"(\{.*\})") 专门抓取大括号之间的内容,就能彻底免疫模型的“废话”。

Q3: 运行 Workflow 时碰到了 WorkflowValidationError: The following events are produced but never consumed 报错是怎么回事?
A: 这是 LlamaIndex 工作流引擎的一种“管道安全检查”机制。
它的意思是:咱们的 check_step 辛辛苦苦生产了一个 NewBlogPostEvent,但是工作流里没有任何一个 Step 声明要接收并处理这个事件,导致事件“无家可归”。

  • 排查方法:检查咱们的消费者函数(在这个例子中是 notify_step)。
  • 解决方案:确保其参数严格加上了对应的类型注解,写成 async def notify_step(self, ev: NewBlogPostEvent)。这样引擎就能自动把上一步产生的数据路由到这里,完成闭环。

Q4: 调用 Groq 或硅基流动 API 时,频繁遇到 429 Too Many Requests 怎么办?
A: 这是因为多智能体循环请求过快,触发了并发限制。
在开发 ReAct Agent 时,模型需要反复执行“思考-行动-观察”的循环,短时间内会产生高密度的 API 请求。

  • 冷却机制:如果是演示项目,可以在每次检测循环之间或者 Step 之间加一个简单的 await asyncio.sleep(2) 进行冷却。
  • 精简上下文:尽量精简 Prompt 和工具返回的数据量,避免单次请求打满 TPM(Tokens Per Minute)限制。如果是生产环境,建议使用硅基流动的付费版以获取更高的并发额度。

Q5: 在生产环境中,我还应该使用 global global_last_title 这种全局变量来管理状态吗?
A: 非常不建议。
本教程使用全局变量仅为了降低代码阅读门槛和方便演示。

  • 状态污染:在实际生产环境中,Workflow 往往是高并发运行的(比如同时监控 10 个不同的 RSS 链接)。如果用全局变量,不同的任务会互相覆盖状态。
  • 正确做法:LlamaIndex Workflow 提供了内置的 Context 对象。咱们应该在 @step 中通过 ctx.set("last_title", title)ctx.get("last_title") 来在不同的步骤之间安全地传递状态,或者引入外部数据库(如 Redis、本地 JSON 文件)进行持久化。