Mingyu's Library主页
AI Daily · 2026-07-07 · 深度学习AI Daily · 2026-07-07 · Deep dive

LlamaIndex Workflows 事件驱动编排:用类型化事件把 Agent 步骤串起来

Event-Driven Orchestration with LlamaIndex Workflows: wiring agent steps together with typed events

基于 LlamaIndex 官方博客、官方文档与社区评测整理。读完你会知道:事件驱动编排怎么写、如何做人在回路与断点恢复、与 LangGraph 怎么选。

Based on LlamaIndex’s official blog and docs plus community reviews. You will learn how to write event-driven orchestration, how to do human-in-the-loop and resume after a crash, and how to choose versus LangGraph.

130 秒速览

1The 30-second version

一句话:LlamaIndex Workflows 是一个事件驱动、async 优先的 Agent 编排库——你写一个个 @step 异步函数,每个函数“接收某种事件、返回另一种事件”,运行时根据函数参数的类型注解自动把事件送到下一个步骤。分支就是 if,循环就是“返回一个更早的事件”,并发就是“返回一个事件列表”。

In one sentence: LlamaIndex Workflows is an event-driven, async-first library for orchestrating agents. You write @step async functions, each of which “takes one kind of event and returns another”, and the runtime routes events to the next step using the type annotation on the function’s parameter. A branch is an if, a loop is “return an earlier event”, and concurrency is “return a list of events”.

为什么今天讲它:7/7 简报的 Agent 技术精选提到 Workflows 1.0“近期讨论升温”,同时 LangGraph 也在推节点级超时与 Saga 补偿。两者代表了 2026 年 Agent 编排的两种主流思路:显式图(LangGraph)与隐式事件路由(Workflows)。弄懂后者,你就能在“我要一张状态机”与“我要几段可组合的 async 代码”之间做出有根据的选择。

Why today: the 7/7 brief’s agent-tech section noted that discussion of Workflows 1.0 was heating up, while LangGraph was shipping node-level timeouts and saga-style compensation. They represent the two mainstream approaches to agent orchestration in 2026: an explicit graph (LangGraph) and implicit event routing (Workflows). Understanding the latter helps you choose, with reasons, between “I need a state machine” and “I need a few composable async functions”.

日期澄清:简报沿用了 Alice Labs 框架对比页“Workflows 1.0 于 2026-06-22 发布、同日 LlamaCloud 更名 LlamaParse”的说法。但一手来源显示:Workflows 1.0 官方公告发布于 2025-06-30(LlamaIndex 博客,作者 Massimiliano Pippi、Logan Markewich);LlamaCloud → LlamaParse 更名见于 2026-02-24 的官方 newsletter。Alice Labs 自己的 Workflows 指南页也写的是 2025-06-30。因此本文把 2026 年 6-7 月的情况理解为“1.0 API 稳定一年后的再讨论”,而非新发布。
Date clarification: the brief repeated the Alice Labs framework comparison’s claim that Workflows 1.0 shipped on 2026-06-22, alongside a LlamaCloud → LlamaParse rename on the same day. Primary sources say otherwise: the official Workflows 1.0 announcement was published on 2025-06-30 (LlamaIndex blog, by Massimiliano Pippi and Logan Markewich), and the LlamaCloud → LlamaParse rename appears in the official newsletter of 2026-02-24. Alice Labs’ own Workflows guide also says 2025-06-30. This article therefore treats June-July 2026 as renewed discussion a year after the 1.0 API stabilized, not a new release.
步骤之间不连线,靠“事件类型”自动路由StartEventplan 步骤search 步骤 #1search 步骤 #2search 步骤 #3num_workers=4:同一步骤最多 4 个副本并发汇合 list[Event]StopEvent循环:返回一个更早的事件类型即可(如结果不够好 → 重新 plan)返回 list[Event] = 扇出
图 1 · 事件如何在步骤间流动(作者据官方文档绘制的示意)
Steps are not wired together; event types route themStartEventplan stepsearch step #1search step #2search step #3num_workers=4: up to 4 copies of a step run at oncejoin list[Event]StopEventLoop: return an earlier event type (weak result → plan again)return list[Event] = fan-out
Figure 1 · How events flow between steps (author’s sketch based on the official docs)

2核心概念

2Core concepts

Event(事件)
一个 Pydantic 模型,继承 Event,字段就是要传递的数据。事件类型就是工作流里的“边”。
StartEvent / StopEvent
每次 run(...) 自动生成 StartEvent,其字段来自 run 的关键字参数;某步骤返回 StopEvent 时运行结束,result 就是 await handler 的返回值。
@step
标记一个 async 方法为处理器;运行时读取参数 ev 的类型注解来决定把哪类事件送给它,读取返回类型来校验流向。
Context(ctx)
一次运行的上下文:发事件(send_event)、写流(write_event_to_stream)、汇合(collect_events)、状态存储(ctx.store)、序列化(to_dict / from_dict)。
类型化状态(typed state)
用 Pydantic 模型声明状态,通过 Context[MyState] 获得类型提示;edit_state() 提供加锁的原子读改写。
Resource(资源注入)
把数据库客户端、LLM、索引等不可序列化对象声明为资源,由运行时创建和注入,不进入快照(1.0 起 Python 支持)。
Handler
run() 返回的句柄:可 stream_events() 看过程、send_event() 往里送事件、await 拿结果。
Event
A Pydantic model subclassing Event; its fields are the data being passed. Event types are the “edges” of a workflow.
StartEvent / StopEvent
Each run(...) creates a StartEvent whose fields come from run’s keyword arguments; when a step returns a StopEvent the run ends and its result is what await handler returns.
@step
Marks an async method as a handler; the runtime reads the type annotation on the ev parameter to decide which events it receives, and the return type to validate the flow.
Context (ctx)
The context of one run: send events (send_event), write to the stream (write_event_to_stream), join (collect_events), the state store (ctx.store), and serialization (to_dict / from_dict).
Typed state
Declare state as a Pydantic model and get type hints through Context[MyState]; edit_state() gives a locked, atomic read-modify-write.
Resource (injection)
Declare non-serializable objects such as database clients, LLMs and indexes as resources; the runtime creates and injects them and keeps them out of snapshots (Python, since 1.0).
Handler
What run() returns: stream_events() to watch progress, send_event() to push events in, and await to get the result.
2025-06-30Workflows 1.0 官方公告
独立仓库与包
Py + TSllama-index-workflows
@llamaindex/workflow-core
2.22.x2026 年中 Python 包版本
(据 Alice Labs)
≈50kllama_index 主仓库星数
2026-06(据 rywalker.com)
2025-06-30Workflows 1.0 announced
standalone repo & package
Py + TSllama-index-workflows
@llamaindex/workflow-core
2.22.xPython package, mid-2026
(per Alice Labs)
≈50kstars on llama_index main repo
June 2026 (per rywalker.com)

1.0 的三个官方新增点(LlamaIndex 博客):类型化状态(Python 与 TS)、资源注入(Python)、可观测性(安装 llama-index-instrumentation 后接 OpenTelemetry、Arize Phoenix 等)。博客强调架构相对 beta 没有大改,主要变化是独立成项目:不装 llama_index 也能用,老的 import 路径通过再导出继续可用。

The three official additions in 1.0 (LlamaIndex blog): typed state (Python and TS), resource injection (Python), and observability (install llama-index-instrumentation to plug into OpenTelemetry, Arize Phoenix and others). The post stresses that the architecture did not change much from the beta; the main change is that it became a standalone project, usable without llama_index, with the old import paths still working through re-exports.

3怎么用:从 Hello World 到并发扇出

3How to use it: from Hello World to concurrent fan-out

安装与最小示例(来自官方仓库 README):

Install and minimal example (from the official repository README):

pip install llama-index-workflows      # Python 3.10+
npm i @llamaindex/workflow-core       # TypeScript

from workflows import Workflow, step
from workflows.events import StartEvent, StopEvent

class HelloWorkflow(Workflow):
    @step
    async def greet(self, ev: StartEvent) -> StopEvent:
        return StopEvent(result=f"Hello, {ev.name}")

# result = await HelloWorkflow(timeout=60).run(name="Ada")
pip install llama-index-workflows      # Python 3.10+
npm i @llamaindex/workflow-core       # TypeScript

from workflows import Workflow, step
from workflows.events import StartEvent, StopEvent

class HelloWorkflow(Workflow):
    @step
    async def greet(self, ev: StartEvent) -> StopEvent:
        return StopEvent(result=f"Hello, {ev.name}")

# result = await HelloWorkflow(timeout=60).run(name="Ada")

注意 ev.name:StartEvent 的字段直接来自 run(name=...) 的关键字参数。timeout=60 是官方入门示例里的写法,用来避免运行无限挂起。

Note ev.name: StartEvent’s fields come straight from run(name=...) keyword arguments. timeout=60 is how the official getting-started example keeps a run from hanging forever.

并发扇出 + 汇合(官方 concurrent execution 文档的写法):一个步骤用 ctx.send_event 发出多个事件,接收步骤用 num_workers 控制并发,下游用 ctx.collect_events 等齐再处理。

Concurrent fan-out + join (as in the official concurrent-execution docs): one step emits several events with ctx.send_event, the receiving step limits concurrency with num_workers, and a downstream step waits with ctx.collect_events until all have arrived.

class ConcurrentFlow(Workflow):
    @step
    async def start(self, ctx: Context, ev: StartEvent) -> StepTwoEvent | None:
        ctx.send_event(StepTwoEvent(query="Query 1"))
        ctx.send_event(StepTwoEvent(query="Query 2"))
        ctx.send_event(StepTwoEvent(query="Query 3"))

    @step(num_workers=4)
    async def step_two(self, ctx: Context, ev: StepTwoEvent) -> StepThreeEvent:
        return StepThreeEvent(result=await search(ev.query))

    @step
    async def step_three(self, ctx: Context, ev: StepThreeEvent) -> StopEvent | None:
        result = ctx.collect_events(ev, [StepThreeEvent] * 3)   # wait for all 3
        if result is None:
            return None
        return StopEvent(result=result)
class ConcurrentFlow(Workflow):
    @step
    async def start(self, ctx: Context, ev: StartEvent) -> StepTwoEvent | None:
        ctx.send_event(StepTwoEvent(query="Query 1"))
        ctx.send_event(StepTwoEvent(query="Query 2"))
        ctx.send_event(StepTwoEvent(query="Query 3"))

    @step(num_workers=4)
    async def step_two(self, ctx: Context, ev: StepTwoEvent) -> StepThreeEvent:
        return StepThreeEvent(result=await search(ev.query))

    @step
    async def step_three(self, ctx: Context, ev: StepThreeEvent) -> StopEvent | None:
        result = ctx.collect_events(ev, [StepThreeEvent] * 3)   # wait for all 3
        if result is None:
            return None
        return StopEvent(result=result)

更简洁的写法:步骤直接返回 list[Event] 即为一个批次的扇出;下游参数写成 list[XxxEvent] 时,会在整批到齐后只触发一次。官方文档还写明了几条细节:批次元素按完成顺序到达(要固定顺序得自己排序);某个 worker 返回 None 会被剔除,汇合照常触发;用 Collect(Take(n)) 包住列表参数可以“先到 n 个就放行”,但不会取消其余任务。num_workers 只限制该步骤的并发副本数,不限制同时有多少次运行。

A shorter form: a step that returns list[Event] fans out one batch; a downstream parameter typed list[XxxEvent] fires once, after the whole batch has arrived. The official docs add a few details: batch elements arrive in completion order (sort them yourself if order matters); a worker that returns None is dropped and the join still fires; wrapping the list parameter in Collect(Take(n)) releases the join after the first n results but does not cancel the rest. num_workers caps concurrent copies of that step, not how many runs execute at once.

类型化状态(官方 state 文档):多个步骤可能同时改状态时,用 edit_state() 做原子更新,并且把慢的 LLM/网络调用移出加锁块。

Typed state (official state docs): when several steps may change state at once, use edit_state() for atomic updates, and keep slow LLM or network calls out of the locked block.

class CounterState(BaseModel):
    count: int = Field(default=0)

class MyWorkflow(Workflow):
    @step
    async def start(self, ctx: Context[CounterState], ev: StartEvent) -> StopEvent:
        async with ctx.store.edit_state() as state:   # locked read-modify-write
            state.count += 1
        return StopEvent(result="Done!")
class CounterState(BaseModel):
    count: int = Field(default=0)

class MyWorkflow(Workflow):
    @step
    async def start(self, ctx: Context[CounterState], ev: StartEvent) -> StopEvent:
        async with ctx.store.edit_state() as state:   # locked read-modify-write
            state.count += 1
        return StopEvent(result="Done!")

4人在回路与持久化

4Human in the loop and durability

人在回路(HITL):官方推荐“两步式”——一个步骤返回 InputRequiredEvent,另一个步骤接收 HumanResponseEvent;调用方在事件流里看到“需要输入”时,把回复送回同一次运行。

Human in the loop (HITL): the docs recommend a two-step pattern. One step returns InputRequiredEvent and another accepts HumanResponseEvent; when the caller sees “input required” in the event stream, it sends the reply back into the same run.

class NumberWorkflow(Workflow):
    @step
    async def ask(self, ev: StartEvent) -> InputRequiredEvent:
        return InputRequiredEvent(prefix="Enter a number: ")

    @step
    async def answer(self, ev: HumanResponseEvent) -> StopEvent:
        return StopEvent(result=ev.response)

handler = workflow.run()
async for event in handler.stream_events():
    if isinstance(event, InputRequiredEvent):
        await handler.send_event(HumanResponseEvent(response=input(event.prefix)))
final_result = await handler
class NumberWorkflow(Workflow):
    @step
    async def ask(self, ev: StartEvent) -> InputRequiredEvent:
        return InputRequiredEvent(prefix="Enter a number: ")

    @step
    async def answer(self, ev: HumanResponseEvent) -> StopEvent:
        return StopEvent(result=ev.response)

handler = workflow.run()
async for event in handler.stream_events():
    if isinstance(event, InputRequiredEvent):
        await handler.send_event(HumanResponseEvent(response=input(event.prefix)))
final_result = await handler

如果提问和回答发生在不同的请求或进程里(现实中的审批流程大多如此),就要把上下文快照下来:提问后保存 handler.ctx.to_dict()(JSON),再 handler.cancel_run(),避免内存里留一个永远等不到答案的运行;答案到了以后用 Context.from_dict(workflow, d) 恢复、workflow.run(ctx=ctx) 继续,再送入 HumanResponseEvent。

If the question and the answer happen in different requests or processes (as most real approval flows do), snapshot the context: after asking, save handler.ctx.to_dict() (JSON) and call handler.cancel_run() so no in-memory run is left waiting for an answer that will never reach it; when the answer arrives, restore with Context.from_dict(workflow, d), continue with workflow.run(ctx=ctx), and send the HumanResponseEvent.

进程 A(请求 1)workflow.run()开始执行返回 InputRequiredEvent流里出现“需要输入”ctx.to_dict() 存库然后 handler.cancel_run()等人类回复进程 B(请求 2,可能数小时后)Context.from_dict()从库里读回快照workflow.run(ctx=ctx)send HumanResponseEventStopEvent返回最终结果恢复是“至少一次”语义:中断时在跑的步骤会重跑,副作用须可重复(据官方文档)
图 2 · 跨进程的暂停—快照—恢复(作者据官方 HITL 与 durable 文档绘制的示意)
Process A (request 1)workflow.run()run startsreturns InputRequiredEventstream shows “input needed”ctx.to_dict() → databasethen handler.cancel_run()human repliesProcess B (request 2, maybe hours later)Context.from_dict()load snapshot backworkflow.run(ctx=ctx)send HumanResponseEventStopEventfinal resultResume is at-least-once: in-flight steps re-run, so side effects must be repeatable (official docs)
Figure 2 · Pause, snapshot and resume across processes (author’s sketch from the official HITL and durability docs)

持久化(durability):官方文档明说工作流“默认是易失的”,run() 返回后状态就没了,而且没有需要打开的内置 checkpointer。做法是在运行中订阅内部事件,每当某步骤结束(StepStateChanged 且 StepState.NOT_RUNNING)就存一次快照。恢复时:已完成的步骤不重跑,挂起的事件会重新派发、汇合缓冲会重建,中断时正在执行的步骤会被回滚重跑——即“至少一次”语义。

Durability: the docs say plainly that workflows are “ephemeral by default”: state is gone once run() returns, and there is no built-in checkpointer to switch on. Instead you subscribe to internal events during the run and save a snapshot whenever a step finishes (StepStateChanged with StepState.NOT_RUNNING). On restore, finished steps do not re-run, pending events are re-dispatched and join buffers rebuilt, and steps that were mid-execution are rewound and run again: at-least-once semantics.

db.save("my-run", json.dumps(handler.ctx.to_dict()))
ctx = Context.from_dict(w, json.loads(db.load("my-run")))
result = await w.run(ctx=ctx)
db.save("my-run", json.dumps(handler.ctx.to_dict()))
ctx = Context.from_dict(w, json.loads(db.load("my-run")))
result = await w.run(ctx=ctx)

不想手写检查点循环?官方提供 DBOS 运行时插件:替换默认的进程内运行时,把步骤状态变化记入数据库,崩溃后自动恢复,工作流代码不用改;WorkflowServer 也能挂同一个插件。

Don’t want to hand-write the checkpoint loop? The docs offer a DBOS runtime plugin that replaces the default in-process runtime, journals step transitions to a database and recovers after a crash without changes to workflow code; WorkflowServer can use the same plugin.

5多智能体、观测与部署

5Multi-agent, observability and deployment

  1. AgentWorkflow(多智能体):把多个 FunctionAgent(各有 name、description、system_prompt、tools、can_handoff_to)组合起来,指定 root_agent,运行时自动注入“交接(handoff)”工具,共享状态放在 ctx.store(据官方 multi-agent 文档与 Alice Labs 指南)。
  2. 观测:pip install llama-index-instrumentation 后,步骤、事件、LLM 调用都会产生 OpenTelemetry span;本地可用 Arize Phoenix 看,生产可通过 OTLP 导出到 Langfuse、Datadog 等(导出目标列表来自 Alice Labs)。
  3. 服务化:server 包把工作流暴露为带流式、持久化、人工审核的 REST API,可挂进现有 Starlette/FastAPI;另有异步 client 供其他服务调用(官方 README)。
  4. 部署:llamactl CLI 从模板脚手架、本地热重载,并可部署到 LlamaParse(原 LlamaCloud)、AWS Bedrock AgentCore 或自有基础设施;可部署成无头服务、MCP server 或带 UI 的全栈应用(官方 README)。
  1. AgentWorkflow (multi-agent): combine several FunctionAgents (each with name, description, system_prompt, tools and can_handoff_to), set a root_agent, and the runtime injects a handoff tool automatically; shared state lives in ctx.store (per the official multi-agent docs and the Alice Labs guide).
  2. Observability: after pip install llama-index-instrumentation, steps, events and LLM calls emit OpenTelemetry spans; view them locally in Arize Phoenix, or export over OTLP to Langfuse, Datadog and others in production (the exporter list comes from Alice Labs).
  3. Serving: the server package exposes a workflow as a REST API with streaming, persistence and human review, and can mount inside an existing Starlette/FastAPI app; an async client lets other services call it (official README).
  4. Deployment: the llamactl CLI scaffolds from templates, hot-reloads locally, and deploys to LlamaParse (formerly LlamaCloud), AWS Bedrock AgentCore or your own infrastructure, as a headless service, an MCP server or a full-stack app with a UI (official README).

6对比:Workflows vs LangGraph

6Comparison: Workflows vs LangGraph

下表综合 Alice Labs 指南(社区/咨询公司观点)与官方文档;“最适合”一行是评论性判断,不是事实。

The table combines the Alice Labs guide (a consultancy’s view) with the official docs; the “best fit” row is opinion, not fact.

LlamaIndex WorkflowsLangGraph
编排模型类型化事件的发布-订阅,隐式路由编译的状态机,显式节点和边
持久化Context.to_dict() 自带存储;或 DBOS 插件内置 checkpointer(Postgres、SQLite、Redis)
调试OpenTelemetry 追踪(Phoenix 等)LangGraph Studio、时间旅行
TypeScript@llamaindex/workflow-core,与 Python 大体对齐(TS 1.0 无资源注入)LangGraph.js(据 Alice Labs 比 Python 落后约一个版本)
多智能体AgentWorkflow 交接;社区评价“较薄”支持完善(JetBrains 评价)
最适合重 RAG/文档处理的流水线需审计的状态机、HITL 检查点
LlamaIndex WorkflowsLangGraph
Orchestration modelPub-sub over typed events, implicit routingCompiled state machine, explicit nodes and edges
PersistenceContext.to_dict() with your own store, or the DBOS pluginBuilt-in checkpointers (Postgres, SQLite, Redis)
DebuggingOpenTelemetry traces (Phoenix etc.)LangGraph Studio, time travel
TypeScript@llamaindex/workflow-core, broadly on par with Python (no resource injection in TS 1.0)LangGraph.js (about one release behind Python, per Alice Labs)
Multi-agentAgentWorkflow handoffs; community calls it “thinner”Well supported (per JetBrains)
Best fitRAG- and document-heavy pipelinesAudited state machines, HITL checkpoints

JetBrains PyCharm 博客(2026-06)的框架盘点把 LlamaIndex 定位为“以数据/检索为中心”:索引和长期记忆强,复杂的重动作编排与多智能体支持有限;LangGraph 控制力、HITL 与多智能体更强。Alice Labs 则建议两者可以组合使用:外层循环用 LangGraph,RAG 子任务交给一个 Workflow。

JetBrains’ PyCharm blog roundup (June 2026) positions LlamaIndex as “data/retrieval-centric”: strong indexing and long-term memory, limited for complex action-heavy orchestration and multi-agent setups, while LangGraph offers more control, HITL and multi-agent support. Alice Labs suggests combining them: LangGraph for the outer loop, a Workflow for a RAG sub-task.

怎么选(作者推断):你的流程本质是“几个异步步骤 + 少量分支/扇出”、核心价值在检索与文档处理 → Workflows 代码量更少;你需要可视化、可审计、带内置检查点和时间旅行的状态机 → LangGraph 更省心。两者不是互斥。
How to choose (author’s inference): if your flow is essentially “a few async steps with some branching or fan-out” and the value lies in retrieval and document processing, Workflows needs less code; if you need a visual, auditable state machine with built-in checkpoints and time travel, LangGraph saves effort. They are not mutually exclusive.

7坑与注意事项

7Pitfalls

官方文档明确写到的:

Stated in the official docs:

  • 至少一次语义:恢复时中断中的步骤会重跑,发邮件、扣款这类副作用要做成幂等。
  • 实例属性不进快照:写在 self.xxx 上的东西恢复后就没了;持久数据放事件或 ctx.store。
  • 全部可序列化:事件与状态里任何一个值不能序列化,to_dict() 就整体失败;客户端、索引、文件句柄放进 Resource。
  • wait_for_event 会重放:单步式等待在恢复时会重跑等待点之前的代码,所以那段代码必须可重复执行;官方因此更推荐两步式 HITL。
  • 批次乱序:list[Event] 汇合按完成顺序到达。
  • 锁内别做慢操作:edit_state() 块要短,LLM 调用移到外面。
  • At-least-once semantics: steps in flight are re-run on resume, so side effects such as sending email or charging money must be idempotent.
  • Instance attributes are not snapshotted: anything stored on self.xxx is gone after restore; keep durable data in events or ctx.store.
  • Everything must serialize: if any value in events or state cannot be serialized, the whole to_dict() fails; put clients, indexes and file handles in a Resource.
  • wait_for_event replays: single-step waiting re-runs the code before the wait point on resume, so that code must be safe to repeat; this is why the docs prefer the two-step HITL pattern.
  • Batches arrive out of order: a list[Event] join receives results in completion order.
  • No slow work under the lock: keep edit_state() blocks short and move LLM calls outside.
社区经验(Alice Labs,未独立核实):① 某分支没有通向 StopEvent 会导致挂起,建议在 CI 里跑 Workflow.validate();② 缓存的 LLM 客户端遇到令牌轮换会中途失败,可用 Resource(factory, cache=False);③ 给状态模型新增必填字段会让旧快照无法恢复,新字段要有默认值并给模型加版本;④ 智能体互相交接可能死循环直到 token 耗尽,应在共享状态里限制轮数;⑤ 只用 ctx.send_event() 的自定义事件可能缺 OTel span,需配合 write_event_to_stream()。
Community experience (Alice Labs, not independently verified): (1) a branch with no path to a StopEvent hangs, so run Workflow.validate() in CI; (2) cached LLM clients can fail mid-run when tokens rotate, so use Resource(factory, cache=False); (3) adding a required field to the state model breaks resuming old snapshots, so give new fields defaults and version the model; (4) agents handing off to each other can loop until tokens run out, so cap turns in shared state; (5) custom events sent only with ctx.send_event() may lack OTel spans, so pair them with write_event_to_stream().
常见误解:“事件驱动 = 没有结构”。恰恰相反,事件类型就是结构:每个步骤的输入/输出类型共同定义了一张图,只是这张图写在类型注解里而不是单独的边定义里。好处是改代码就是改图;代价是要看清全貌得靠绘图工具或追踪。
A common misreading: “event-driven means unstructured”. The opposite is true: the event types are the structure. Each step’s input and output types together define a graph; it lives in type annotations rather than in separate edge definitions. The upside is that editing code edits the graph; the cost is that seeing the whole picture takes a drawing tool or traces.

8资源与来源

8Resources and sources