在事件驱动的智能体系统中,我们需要换一种方式理解“谁调用谁”。传统函数调用关心调用者和被调用者,而智能体运行时(agent runtime)更关心事件从哪里产生、由谁消费、控制权何时让出,又在什么条件下恢复。

许多大语言模型智能体框架,默认采用一种“调用—返回”的思路:用户提出请求,模型生成工具调用,工具执行完毕后返回结果,模型再继续推理。这个模型清楚,也容易实现。对于短暂、边界明确的一次性任务,它本来就很合适。

问题出现在控制流不再是一条线的时候。工具可能长时间运行,日志会持续产生,用户会中途操作,后台任务也可能自己触发事件。此时,与其追问“模型是不是主程序”,不如问得更具体:谁在产生事件,谁在消费事件,状态由谁持有,下一步又由谁决定?

从这个角度看,通信顺序进程(Communicating Sequential Processes,CSP)1和协程(coroutine)2提供了两个不同层次的视角。CSP 是一种通信与并发模型,强调独立过程怎样通过消息协作;coroutine 则更接近一种执行机制,让一个执行单元在等待时暂停,把控制权交回 runtime,等条件满足后再继续。两者不能互相替代,但都能帮助解释单一 call stack 难以表达的控制流。

模型是不是主程序

在传统程序里,主程序负责控制执行顺序。它调用函数,等待返回值,然后继续下一步。如果把这种理解套到智能体上,就会得到一种直觉:大语言模型是主程序,工具只是被它调用的函数。

这个直觉在短调用里没有问题。模型确实会提出工具请求,也会根据结果继续规划。但在事件驱动系统中,很多变化并不是模型主动发起的:用户可以随时补充指令,工具会在完成时产生结果,observer 会报告异常,scheduler 也可能在时间条件满足时触发任务。模型只是事件来源之一,不需要承担整个系统唯一的控制中心。

因此,在这类系统里,把模型看成 planner 往往更准确。它负责解释变化对目标意味着什么,再决定继续等待、请求确认、启动工具、调整计划还是终止流程。它处在高层决策位置,但并不站在整套系统唯一的调用栈顶端。

通信过程视角

CSP 的重点不在“谁调用谁”,而在独立过程怎样通过消息协作。用这种视角看 Agent runtime 时,可以把需要独立推进的参与者建模为 process。每个 process 维护自己的局部状态,通过 channel 与其他 process 交换消息,不需要共享同一条调用栈,也不需要以层层返回值的方式协作。

下文用小写 process 表示 CSP 意义上的通信过程。它描述的是 control flow 和 communication,不等同于 《Structured LLM Design》 中按生命周期定义的大写 Process。后者还有一个更严格的条件:它可以超出 creator scope 继续存在。

例如,在通信模型里,可以把用户界面、planner 的控制过程、工具执行器、日志 observer、scheduler,甚至外部服务,都视为能够独立产生或接收消息的参与者。这里并不要求它们在实现上都运行成同一种 process;这个抽象只强调它们可以独立推进,并通过事件交换状态变化。

这些参与者可以通过 channel 或其他消息机制交换事件。用户操作、工具请求、工具结果、日志和 observation 可以沿不同路径传播。关键不在于给每一种消息都造一个新的顶层抽象,而在于发送方和接收方不必共享同一条调用栈,也不必同时处于运行状态。

一个典型流程因此不必再写成“用户调用模型,模型调用工具,工具返回结果”。用户界面可以先产生操作事件;runtime 更新状态后,在需要规划时唤醒 planner;planner 发出工具请求;工具执行器工作过程中继续产生状态变化;观察层再判断哪些变化值得形成新的 observation。

在这种协作里,不必把所有关系都压成一条 call/return 链。短操作仍然可以使用普通调用;持续活动则可以在推进过程中不断产生事件。工具完成、日志更新、用户追加输入和定时触发都可能成为新的事件来源。Planner 不是压在所有组件之上的主函数,而是由 runtime 在需要高层解释和规划时调用的决策组件。

这种模型尤其适合持续运行和多事件源的场景:长任务、服务日志、用户中途输入和设备反馈都可能独立推进。单一调用栈很难同时表达这些关系,而显式的消息边界能更清楚地表示谁产生状态、谁消费状态,以及谁可以独立等待。

日志流为什么适合通信模型

日志流尤其能说明这种模型的价值。

在传统调用思路里,规划器似乎需要主动请求日志,然后等待一段结果。但实际服务不会等模型来调用才产生日志,它本来就在持续运行。更合理的结构是由日志观察器持续消费日志流,先完成筛选和聚合,再把与当前任务相关的状态变化形成 observation。

这样一来,规划器不需要直接盯住日志系统。它只处理已经被提炼成 observation 的变化。日志观察器负责判断哪些日志与当前任务相关、哪些只是背景噪声,并区分普通进展、错误、超时和可能需要重新规划的信号。是否真的改变计划,仍由 planner 根据目标和上下文决定。

通信模型的好处就在这里:各个过程先处理自己的输入,只把有用的状态变化送给其他过程。Planner 不需要盯住所有原始信号,也不需要主动轮询每个组件。

在这种结构中,observer 也可以单独作为一个过程运行。它不是 planner 的缩小版,而是负责监测、筛选和上报。它持续消费日志流或工具输出,再把压缩后的状态变化发送给 planner。

从 I/O readiness 到语义事件

一个容易混淆的地方是把 Agent 的“事件驱动”直接等同于 epoll、kqueue 或 Go runtime 的 netpoller。这个类比只能借用“同时等待多个来源”和 I/O readiness 这一层,不能把操作系统事件机制直接当成智能体的语义事件模型。

操作系统只回答“这个文件描述符现在能不能读或写”。它不知道读到的字节意味着 CI 已经完成、用户改了约束,还是工具正在请求确认。协议层先把字节解析成消息,runtime 再把消息解释成事件,观察层最后才把必要的事件压缩成有决策意义的 observation。

epoll / kqueue readiness
          ↓
transport bytes
          ↓
protocol message
          ↓
runtime event
          ↓
semantic observation

因此,Go 的 select 可以帮助理解控制流:runtime 能同时等待多个来源,并处理已经 ready 的 channel。但这种类比也到此为止。ready 只说明某个输入可处理,不说明它具有决策价值;从 runtime event 到 semantic observation,还需要额外的筛选、关联和状态解释。

协程视角下的控制权转移

协程提供的是更接近实现层的视角。一个执行单元可以在等待某个结果时暂停,把控制权交还给 runtime;条件满足以后,再从暂停点继续。它可以用来实现某些 CSP-style 的过程,但协程本身并不规定系统应该采用怎样的通信语义。

如果 runtime 用 coroutine 组织控制流,负责高层规划的控制协程可以在没有 observation 时暂停。收到值得处理的 observation 后,它再调用大语言模型、得到 action,并把 action 分发给对应执行器。等待工具结果时,暂停的是相关协程,而不是整个系统;runtime 仍然可以继续调度 observer、工具执行器、界面处理器和 scheduler。

工具执行器也可以用 coroutine 表达:等待请求,开始执行,过程中发送 progress,结束时发送结果。Observer 持续消费日志流,scheduler 等待时间条件。它们在等待时都可以把控制权交回 runtime。

从代码写法上看,协程有时像同步流程,因为可以写成“等待某个事件后继续”。但从系统逻辑上看,它仍然是事件驱动的。等待并不意味着整个系统停住,而只是当前协程暂停。其他协程仍然可以继续处理用户输入、工具进度、日志异常或定时触发。

协程里的暂停与恢复描述的是 runtime 的控制流,不必等同于“把某个正在执行的 LLM frame 挂起十分钟,再从同一个机器栈帧继续”。对于会逃逸当前调用范围的活动,更自然的结构往往是:

Invocation A
    │
    └── start long activity
             │
             └── handle returned

Invocation A ends

activity continues
      │
      └── Completed event
               │
               ▼
            runtime
               │
          new observation
               │
               ▼
          Invocation B

Invocation A 并没有神秘地复活;只是外部世界发生了新的变化,runtime 因此启动一次新的计算。这种区分对长时间运行的 CI、部署和远程任务尤其重要。

没有固定的发起方

传统结构可以概括为:主程序调用大语言模型,大语言模型调用工具,工具返回,大语言模型继续。这个结构把模型放在中心,也把所有交互压成一条线。

在事件驱动系统中,没有一个永久固定的发起方。用户输入可以启动一轮处理,工具完成会产生新事件,observer 可以报告异常,scheduler 可以按时间触发,外部服务也可能独立发生状态变化。每个具体事件都有来源,但大语言模型并不总在事件链的起点。

这样,模型计算可以集中在需要语义判断的地方:错误是否影响目标,用户输入是否改变约束,工具结果是否足以进入下一步,异常是否需要人工确认。事件采集、传递、聚合和简单过滤则可以由普通程序完成。

因此更自然的中心是 runtime 的事件循环和状态更新,而不是某一次模型调用。Planner 处理高层决策,工具执行器处理外部工具,observer 处理日志和状态变化,scheduler 处理时间触发,界面处理用户输入。

规划器也不需要持续轮询所有信息。Runtime 可以接收用户事件、observation、工具结果和调度事件,再根据当前状态决定是否需要 planner 介入。不是每个到达的事件都必须直接触发一次模型调用。

这仍然可以类比 Go 的 select:系统同时等多个 channel,哪个先有事件就先处理哪个。相比一条单线程调用链,这种写法更容易表达并发、等待、中途取消、错误恢复和流式输出。

长时间运行的工具更适合先启动活动,再返回 handle

短工具调用没有必要放弃 call/return。需要换一种控制流表达的,是那些会持续产生状态,或者需要独立推进的活动。与其让调用者一直停在“等待最终返回值”这一点上,不如先启动活动并返回稳定的 handle,再通过后续事件观察它。

模型此时表达的不是“调用这个工具并立即得到结果”,而是“启动这个活动”。执行器接收请求后返回 activity ID 或 handle;活动随后可以继续产生进度、日志、中间结果、错误和完成事件。Planner 可以继续等待,也可以取消、重试、切换方案,或根据新的 observation 重新规划。这里的 activity 和 handle 是控制流描述,不预先决定它在生命周期模型中应被归类为 Task 还是 Process。

这样,原本挤在一个同步调用里的几件事就被拆开了:progress 可以逐步处理,取消可以单独发送,observer 可以在异常时触发恢复,多个工具也可以并发推进。Planner 不必为了等待一个活动而占住整条控制流。

从控制流角度看,这个变化把等待从模型调用链中移了出来。Runtime 可以在活动继续推进时处理其他事件,并在完成、失败、超时、取消或出现其他关键 observation 时,再决定是否唤醒 planner。

在状态不完全可见的环境中进行控制

再往前一步,可以把 Agent runtime 看成一个在信息不完整的环境中持续决策的系统。环境不会把完整状态直接交给模型,系统只能从用户输入、工具结果、日志、指标和传感器反馈中逐步更新判断。在控制理论、机器人和部分决策模型中,这类限制常被称为“部分可观测”。这里借用的是“系统只能通过 observation 间接认识环境状态”这一性质。

规划器的任务不是简单地把输入变成输出,而是不断接收观察,更新对环境的判断,再选择下一步行动。行动执行后,环境发生变化,又产生新的观察。这个循环不断重复。

这与机器人系统有一个有限的相似点:系统都无法直接获得环境的完整真实状态,只能根据观察更新内部判断。大语言模型智能体也常面对不断变化、信息不完整、反馈可能延迟的环境。日志、工具结果和用户动作都是证据来源,但任何单一来源都不等于完整状态。

因此,当 Agent 持续面对变化中的环境时,“输入进去,输出出来”只能描述单次计算,不能描述整个系统。完整循环是观察、更新判断、行动,再观察。模型负责高层规划,runtime 负责把反馈组织成可靠的事件和 observation。

结论

对于包含大语言模型、工具、日志流、用户行为和自动化动作的智能体系统,用同步调用模型来理解会越来越困难。它把系统压成一条线,却无法自然表达长任务、流式输出、持续日志、并发操作和环境反馈。

更合适的方式,是把智能体运行时看成一个由多个独立活动共同推进的事件系统。用户界面、工具执行器、observer、scheduler 和 planner 可以在通信模型中被视为独立参与者;实现上则可以使用 coroutine、task、线程或其他机制组织执行。大语言模型承担高层规划职责,但不是唯一主程序。

CSP 强调独立过程怎样通过消息通信;coroutine 强调一个执行单元在等待时怎样让出控制权,并在条件满足后恢复。前者主要是通信模型,后者主要是执行机制。它们关注的层次不同,但都说明了一件事:当 Agent 持续面对多来源、异步变化的环境时,一条线性的 call/return 链不足以描述完整控制流。

本文讨论的是 communication 和 control flow,不是生命周期分类。异步、streaming、使用 channel,甚至被建模成 CSP process,都不会自动让一个活动变成大写的 Process。后者只看一个判据:creator scope 结束以后,它是否还能继续存在。这个边界我在 《Structured LLM Design:按生命周期组织 Agent Runtime》 中进一步展开。

一旦长工具通过 handle 持续暴露状态、日志被整理成 observation、用户输入和定时触发也进入 runtime,“谁调用谁”就不再有一个永久固定的答案。更值得追踪的是:每个事件从哪里来,状态由谁持有和更新,等待时哪个执行单元让出控制权,以及什么条件会触发下一次计算。

Footnotes

  1. 通信顺序进程(Communicating Sequential Processes, CSP)是一种并发系统模型,用独立过程之间的通信来描述协作。本文借用它理解消息边界和控制流,并不要求具体实现严格采用某一种 CSP runtime。 ↩

  2. 协程(coroutine)是一种可以暂停和恢复的执行单元。它适合表达异步等待和流式处理:一个协程暂停时,runtime 可以调度其他执行单元继续工作。长期活动是否应继续存在,则是另外的生命周期问题。 ↩