返回资讯中心
外部精选
软件工程
#开发者实时流

Cadenya 详解如何用 Temporal 构建 AI 代理循环

来源标题:Building an Agentic Loop in Temporal

Cadenya 在托管运行时中基于 Temporal 驱动用户代理,并发布指南拆解其工作流与活动如何组成代理循环。文章介绍了人工审批挂起、消息排队和触发上下文压缩等机制,代码示例取自其 Go 代码库。代理、变体与目标状态存储在 Postgres 中。

Hacker News 最新btables2026/11/14 · 今天30 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

我们是一群热爱开发的开发者。我们的目标是打造一个yes-code平台,让构建智能体的体验成为软件开发中最令人愉快的部分。

在Temporal中构建智能体循环 实战讲解 2026年11月14日

$ whoami

Robert Ross $ pwd Cambridge, MA

Cadenya在底层使用Temporal,在我们的托管运行时中为用户智能体提供动力。本指南将带你了解我们如何运行一个Temporal AI智能体:背后的workflow和activity,以及它们如何组合成一个智能体循环。我们会讨论如何暂停智能体循环以等待人工介入审批、消息排队,以及触发压缩。Cadenya用Go编写,因此本文中的所有代码片段都直接来自我们的代码库(接近但不完全一致)。系好安全带!

Cadenya将智能体、变体和目标存储在Postgres中。我们的许多Temporal workflow和activity都是在数据库状态与循环之间来回协调。Temporal为workflow维护的许多“状态”都会镜像到数据库中,并额外增加了中断等状态。下面的数据库ERD展示了各表之间(错综复杂的)关系。

erDiagram agents ||--o{ objectives : "" agent_variations ||--o{ objectives : "" objectives ||--o{ objective_context_windows : "" objectives |o--o| objective_context_windows : "" objectives ||--o{ objective_events : "" objectives ||--o{ objective_iterations : "" objectives ||--o{ objective_tool_calls : "" objectives ||--o{ objective_tools : "" objectives ||--o{ objective_queued_actions : "" objectives ||--o{ objective_secrets : "" objectives ||--o{ objective_feedbacks : "" objectives ||--o{ objective_webhook_deliveries : "" objective_context_windows ||--o{ objective_events : "" objective_context_windows ||--o{ objective_iterations : "" objective_context_windows ||--o{ objective_tool_calls : "" objective_context_windows ||--o{ objective_context_window_tools : "" objective_tools ||--o{ objective_context_window_tools : "" objective_events |o--o{ objective_tool_calls : "" objective_events |o--o{ objective_queued_actions : "" objective_events ||--o{ objective_webhook_deliveries : ""

该图表无法加载。其源码如上所示。

智能体循环需要一个入口点。Cadenya的顶层Temporal workflow名为AgentObjectiveWorkflow,只接受一个参数:一个包装了Objective的AgentObjectiveWorkflowInput protocol buffer。关于API设计的更多内容,请阅读这篇博客文章。

我们的AgentObjectiveWorkflow负责:

workflow的函数签名只接受一个protocol buffer类型的输入。该消息定义如下:

message AgentObjectiveWorkflowInput { // objective is the objective being executed. cadenya.api.v1.Objective objective = 1; // context_window is the initial context window for this execution. cadenya.api.v1.ObjectiveContextWindow context_window = 2; // max_iterations caps the total number of LLM iterations. 0 means unlimited. int32 max_iterations = 3; // total_iterations tracks iterations completed across all executions. // Incremented during the loop and forwarded on continue-as-new. int32 total_iterations = 4; // total_context_windows tracks context windows used across executions. int32 total_context_windows = 5; // total_tool_calls tracks tool calls dispatched across all executions of // this workflow, forwarded on continue-as-new. Compared against the // variation's constraints.max_tool_calls to enforce the limit. int32 total_tool_calls = 8;

在Temporal UI中,workflow的一次运行如下所示:

我们从一个名为ObjectiveHeartbeatWorkflow的Temporal workflow发送心跳。它在AgentObjectiveWorkflow启动时立即作为子workflow启动,然后被“后台化”,以便workflow的其余部分可以继续执行。它的职责非常简单:向任何已连接的SSE处理器发送心跳。它是我们最简单的workflow:

// internal/objectiveworker/heartbeat_workflow.go const heartbeatInterval = 5 * time.Second const heartbeatPulsesPerRun = 100

func ObjectiveHeartbeatWorkflow(ctx workflow.Context, input *objectiveworkflowsv1.ObjectiveHeartbeatWorkflowInput) (*objectiveworkflowsv1.ObjectiveHeartbeatWorkflowOutput, error) { actx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ ScheduleToCloseTimeout: 5 * time.Second, RetryPolicy: &temporal.RetryPolicy{MaximumAttempts: 1}, })

for range heartbeatPulsesPerRun { if err := workflow.Sleep(ctx, heartbeatInterval); err != nil { return nil, err }

pulse := (&objectiveworkflowsv1.EmitObjectiveHeartbeatActivityInput_builder{
  ObjectiveId: input.GetObjectiveId(),
  WorkspaceId: input.GetWorkspaceId(),
  Compaction:  input.GetCompaction(),
}).Build()

var out objectiveworkflowsv1.EmitObjectiveHeartbeatActivityOutput
err := workflow.ExecuteActivity(actx, persistence.Activity.EmitObjectiveHeartbeat, pulse).Get(ctx, &out)
if ctx.Err() != nil {
  return nil, ctx.Err()
}
if err != nil {
  workflow.GetLogger(ctx).Warn("heartbeat activity failed", "error", err)
}
if out.GetStop() {
  return new(objectiveworkflowsv1.ObjectiveHeartbeatWorkflowOutput), nil
}

}

// Continue as new so a long-running objective never grows this history. return nil, workflow.NewContinueAsNewError(ctx, ObjectiveHeartbeatWorkflow, input) }

这些心跳会落入任何连接到Cadenya API的已打开服务器发送事件流中。你可以从这篇文章了解更多关于我们SSE架构的内容。

在Cadenya中,当循环逐步处理LLM返回的各个轮次时,objective的状态会发生变化。当objective最初创建时,它处于PENDING状态,随后会由于我们workflow中的第一个activity而迅速变为RUNNING。智能体经过的每个事件都会存储在objective_events表中,并推送到Google Cloud Pub/Sub以触发webhook等副作用。Cadenya中的每一个事件都可以作为webhook接收,而我们并非从objective事件直接发送webhook,而是使用事件驱动架构来分离这一关注点。

至此,我们的AgentObjectiveWorkflow已经具备:

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →