news 2026/9/17 5:57:40

从退款审批 Agent 原型实测 Think API:Turns / Actions / Channels 三大 RFC 的落地验证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从退款审批 Agent 原型实测 Think API:Turns / Actions / Channels 三大 RFC 的落地验证

从退款审批 Agent 原型实测 Think API:Turns / Actions / Channels 三大 RFC 的落地验证

【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents

导读:design/think-ops-demo-findings.md是一份"时点性发现记录",记录了在 Cloudflare Think(@cloudflare/think当前已发布 API之上构建一个真实的非编码型运营 Agent(退款/纠纷处理)时,逐一暴露出的 API 人体工学缺口,并将每个缺口映射到 Turns、Actions、Channels 三份姊妹 RFC。读完本文,你将理解:生产级副作用(授权、审批、幂等)为何是 Think 演进的核心议题;action()runTurn()deliverNotice()等新原语分别解决了原型中的哪个GAP(...)痛点;以及这些 API 在当前仓库源码中的真实落地形态。

一、原型背景:一个"动钱"的运营 Agent 需要什么

原型experimental/ops-approval-agent是一个非编码型(non-coding)发现原型:一个退款/纠纷运营 Agent。它处理一个非常典型的工作流——人工或上游系统请求处理退款,Agent 先查订单,再执行退款。关键点在于:退款是真实动钱(money-moving)的副作用,因此必须具备三个生产级保证:

  1. 权限门控(permission-gated)——不是任何会话都能发起退款;
  2. 人工审批(human-approved)——退款必须先经审批;
  3. 幂等(idempotent)——重试或重新部署绝不能导致重复付款。

这三个保证正是"今天"的 Think 原语最吃力的地方。原型源码位于 experimental/ops-approval-agent/src/server.ts,其中每个GAP(...)注释都对应 findings 文档中的一条结论;原型对真实的@cloudflare/thinkAPI 通过类型检查,完整运行方式见 experimental/ops-approval-agent/README.md。

原型的控制面是无 UI 的纯 HTTP 接口(fetch路由到 Agent stub),包括:

端点作用
POST /ops/grant?session=S授予 scopes({ scopes: string[] }
POST /ops/request-refund?session=S提交退款请求{ orderId, amountCents, reason }
POST /ops/inject-context?session=S注入带外上下文{ note }(webhook 场景)
GET /ops/inspect?session=S&id=ID查询 submission 状态
GET /ops/debug?session=S导出 transcript + ledger + 已授予 scopes

模型采用@cf/moonshotai/kimi-k2.7-code,系统提示词约束了工作流:"先查订单确认可退,再发起退款,退款金额不得超过订单总额"。

二、今天做得好的部分:原语的"正确之处"

findings 文档首先客观记录了当前原语在哪些地方是恰如其分的,这决定了 RFC 是"锦上添花"而非"推倒重来":

  • submitMessages()的持久化接受(durable acceptance)——完全适合"现在接受请求、后台跑回合、稍后查状态"的模式。submissionId+idempotencyKey+metadata的三元组无需任何 workaround。原型中requestRefund直接调用它,用submissionId同时作为提交的幂等键,并把kind: "refund-request"及完整入参放进metadata
  • addMessages()(新的 quick-win API)——无回合地注入带外上下文,webhook 路径只有一行代码。它的短板在作用域而非人体工学(见 Channels 一节)。原型injectContext用一行this.addMessages([...])完成"让模型下一回合看到升级信息"。
  • getScheduledTasks()——让"每日主动摘要"这种 proactive 触发器声明起来极为简单。原型用它声明了dailyRefundDigest(每 24 小时跑一次,让模型总结近期退款并标记异常大额)。
  • tool({ needsApproval })——能正确地把回合停泊approval-requested状态。缺口在于"解决(resolve)"而非"请求(request)"。

一句话概括:请求审批的原语是对的,解析审批的原语是缺的——这是整个 demo 最尖锐的发现。

三、Findings → Actions RFC:生产副作用缺的三种能力

退款工具需要每个生产级副作用都具备的三样东西,原型里全部是手搓的。这三条逐一验证了 design/rfc-think-actions.md 的核心提案。

3.1 权限 / 授权:没有一等公民的"此动作需要 scope X"

当前 Think 中,权限声明的唯一拦截点是命令式的beforeToolCall钩子(RFC 中标注think.ts:3479),每个应用都得自己实现。原型把grantedScopes存在 Agent state 里,在每个副作用execute的开头调用私有方法_requireScope(),被拒时抛出一个字符串错误:

private _requireScope(scope: string) { const granted = this.state?.grantedScopes ?? []; if (!granted.includes(scope)) { throw new Error( `Permission denied: this agent has not been granted "${scope}". ` + `Grant it via POST /ops/grant before retrying.` ); } }

findings 指出该方案的三个具体问题:

  • 每个工具都要重复这段样板,新工具很容易忘记加;
  • 模型看不到哪些工具会改状态——没有读/写元数据,模型无法在推理时对"危险工具"保持谨慎;
  • 拒绝是一个非结构化的 thrown error,不是可重放、可检查的结果。

这恰好验证了 Actions RFC 的action({ permissions })+authorizeTurn()以及结构化拒绝结果的设计。RFC 的授权模型是两半:动作声明"需要什么"(permissions: string[]或谓词),调用方声明"拥有什么"(每回合解析一次的AuthorizationContext,通过可覆写的authorizeTurn/authorizeAction钩子注入);默认行为是全量授权(full grant),保证对存量应用零行为变更。拒绝时动作不执行,模型收到结构化工具输出{ error: { name: "ActionAuthorizationError", ... } },可以据此向用户解释而非崩溃。

3.2 幂等:手搓的 SQLite ledger 与输入派生幂等键

发起退款必须"跨重试、跨重新部署恰好结算一次"。原型手搓了一张cf_demo_refund_ledgerSQLite 表,从(order, amount)派生幂等键,并在 execute 中分支判断"已退款则返回 already_issued":

const idemKey = `refund:${orderId}:${amountCents}`; const existing = this.sql<RefundLedgerRow>` SELECT * FROM cf_demo_refund_ledger WHERE idem_key = ${idemKey} `; if (existing.length > 0) { return { status: "already_issued", idempotent: true, refundId: existing[0].refund_id }; }

这正是 Actions RFC 的cf_think_action_ledger+action({ idempotency })要做的事。findings 还给 RFC 提了一个重要精化

此处自然的幂等键是从工具"输入"派生的,而不是由调用方提供的——RFC 应把输入派生键做成头等选项(例如idempotencyKey: (input) => string),而不只是调用方传入的字符串。

这一建议已被 RFC 采纳:其ActionConfig.idempotencyKey类型即为string | ((input: Input) => string),示例中refundPayment动作就写着idempotencyKey: ({ paymentId, amount }) => \refund:${paymentId}:${amount}`。同时 RFC 强调了一个重要边界:默认回退键tool:${toolCallId}只在保留 tool call id 的路径(审批续跑、自动续跑)上有效;跨恢复重试、跨 submission、跨 webhook 的去重必须提供由**稳定业务数据**派生的语义键,且不得把ctx.requestId`、时间戳或随机值编进键里。

3.3 审批解析:最尖锐的缺口

needsApproval: true能把回合停泊住,但Think 当前没有服务端 / 编程式地解析一个通用工具审批的能力。唯一的 approve/reject API(approveExecution/rejectExecution,RFC 标注think.ts:9133)是 codemode execute 运行时专用的。对退款场景来说,真实的审批人是一个后端工作流(经理在内部工具上点批准),而不是聊天客户端,因此今天后端无法干净地批准。

findings 对此的结论是:强烈验证了 Actions RFC 的"稳定审批描述符"(stable approval descriptor),且它必须附带服务端可调用的解析路径,而不只是客户端/WebSocket 路径。RFC 给出的ActionApprovalDescriptor结构如下,让 web、语音、messenger 各种前端渲染同一份数据:

type ActionApprovalDescriptor = { requestId: string; toolCallId: string; action: string; summary: string; // 模型/应用提供的"将要发生什么"摘要 input: unknown; permissions: string[]; risk?: "low" | "medium" | "high"; // 给 UI 的粗略风险提示 kind: "approval-gated" | "durable-pause"; };

原型issue_refund工具明确写着GAP(actions)needsApproval: true停泊回合后,"本原型中回合停在这里,就一直停着"——审批回路端到端无法完成,这本身就是 headline finding。

四、Findings → Turns RFC:启动一个回合的三扇门

原型在"启动回合"这件事上遭遇了 API 表面分裂,验证了 design/rfc-think-turns.md 的runTurn()统一模型:

  • 三扇门启动同一个逻辑事件。"处理这笔退款"这一件事,可以走submitMessages(持久化接受)、saveMessages(阻塞等待),或 WebSocket 聊天路径(人类);而主动工作则是getScheduledTasks里的一串自然语言prompt。原型只需要submitMessages,但在三扇门之间做选择,必须预先知道各自的准入/返回语义。findings 的结论:验证了runTurn()统一 trigger + admission + body 的提案。
  • 定时任务 prompt 是 stringly-typed 的触发器。getScheduledTasks里一个定时任务是一段自然语言字符串,与结构化的submitMessages形态脱节。统一的runTurn({ trigger: "scheduled", ... })能让定时与编程式回合共享同一套 body / 类型化输入。原型代码如下:
getScheduledTasks(): ThinkScheduledTasks { return { dailyRefundDigest: { schedule: "every 24 hours", prompt: "Summarize how many refunds were issued in the recent ledger and " + "flag any that look unusually large." } }; }
  • **审批续跑在 API 层面不可见。**审批通过后回合必须继续,但今天这种续跑是内部机制,用户侧调用点拿不到任何句柄。Turns RFC 把"续跑"显式建模为头等回合(由 action ledger 保证已批准的副作用不会重跑),正是 demo 需要的模型。

从 Turns RFC 的实现看,runTurn(options)mode: "wait" | "submit" | "stream"分别委托给现有的saveMessages/submitMessages/chat路径,返回值保持兼容;内部TurnSpec+_admitTurn把七条准入路径收敛到同一套 admission 例程,并加入了"回合内阻塞式wait会死锁"的 async-local 守卫。addMessages()则以append/upsert两种模式写入 Session 树,不进回合队列、不启动推理,因此可以在工具执行内部安全调用。

五、Findings → Channels RFC:缺失的"带外通知"原语

纠纷 webhook 需要做两件事:(a) 让模型看到升级信息,(b) 通知运营人员。今天的实现是两个互不相干的机制addMessages()(模型看到)和 broadcast(web 客户端看到)——而非 web 的表面(email / Slack / voice)两者都拿不到。findings 的结论:

验证了deliverNotice({ informModel })是缺失的统一体,且它必须以 channel 为目标,而不只是 web WebSocket 客户端。

此外,addMessages注入的是一条user消息——没有 "system/notice" 来源标记(provenance),模型可能把运营上下文误读成客户在说话。findings 对 Channels RFC 提出精化:notice 应携带与 user 回合区分开的来源标记

这两条都落进了 design/rfc-think-channels.md 的 v1 实现:deliverNotice(text, { channel, informModel, kind, thread })是不启动模型、不进入恢复事故路径(never an incident)的投递;informModel: true时以"[Delivered to the user out of band]"标准注解写入 transcript,让下一回合不会重复或反驳。原型injectContext目前只能做到模型可见的一半:

async injectContext(note: string) { await this.addMessages([ { id: crypto.randomUUID(), role: "user", parts: [{ type: "text", text: `[ops-context] ${note}` }] } ]); return { injected: true }; }

Channels RFC 同时把原生 web 聊天与 voice 建模为与 messenger 平等的 channel(内置隐式webchannel),并新增显式投递标签DeliveryKind = "final" | "interim" | "notice" | "command"turnEnded标志,避免从文本形态推断投递生命周期。

六、横切结论:60/40 与三大 RFC 的收敛点

findings 的 cross-cutting 部分给出了两个战略级判断:

  1. **60% 管线 vs 40% 领域逻辑。**这个 Agent 约 60% 的代码是"生产语义管线"(授权、ledger、审批接线),只有约 40% 是领域逻辑。这个比例正是整个 API 战略的论点:Think 应该拥有管线,让应用代码主要是领域逻辑。原型server.ts的代码量对比很直观——_requireScopecf_demo_refund_ledger建表与分支、审批停泊的注释说明占了大部分篇幅。
  2. **三大 RFC 收敛于一个持久化概念。**一个"被授权、被批准、恰好结算一次、可续跑"的副作用——即action ledger + 回合续跑对其他两者是承重墙。findings 建议的落地顺序是 Actions(ledger + 审批描述符)先于 Channels,因为 notices / continuations 都依赖"恰好结算一次"(settled-once)语义。这也与三份 RFC 文档推荐的Turns → Actions → Channels顺序一致。

七、从 GAP 到落地:源码中的实现印证

findings 是"时点性发现记录",它验证的三份 RFC 在仓库中均已实现。在 packages/think/src/think.ts 中可以直接看到:

  • action()已发布think.ts:1484):返回带ACTION_BRAND品牌的冻结描述符,并内置了kind: "durable-pause"approval: false互斥的编程错误校验;配套的isAction()类型守卫在think.ts:1503ActionConfig类型在think.ts:1451附近,包含permissionsapproval(布尔或按输入求值的谓词)、idempotencyKeytimeoutMskind等字段。
  • getActions()钩子think.ts:4451)默认返回{},与getTools()并列;_actionToToolthink.ts:6906)把 Action 描述符编译为普通 AI SDK 工具,因此能流经既有的beforeToolCall/afterToolCall包装路径——原型中手搓的授权检查、幂等分支、审批停泊由此变成框架级声明式能力。
  • configureChannels()已发布think.ts:4468):文档注释明确"包装而非取代getMessengers()",隐式webchannel 始终存在,collision 时报错。

测试侧同样有对应证据:packages/think/src/tests/actions-durable-pause.test.ts验证"以同一幂等键第二次 park + approve 时,ledger 直接回放已结算结果而不重跑 execute";packages/think/src/tests/actions-attach-reply.test.ts验证"action ledger 重放时不会重新触发 attachments";packages/think/src/e2e-tests/action-ledger-recovery.test.tsaction-pause-recovery.test.ts则覆盖了崩溃恢复场景下的幂等与停泊恢复。这些测试正是 RFC "Testing strategy" 中"settled key replays stored result without re-executing"与"no double side effect across a crash"的具体落地。

八、原型怎么跑:复现这份发现

按 experimental/ops-approval-agent/README.md 可复现原型(无 UI、纯 HTTP 控制面):

pnpm install cd experimental/ops-approval-agent pnpm run dev

然后依次驱动控制面,即可重现文档中的每一条发现:

# 1. 授予工具所需 scope(今天仍是手搓授权) curl -X POST 'http://localhost:8787/ops/grant?session=demo' \ -d '{"scopes":["orders:read","refunds:write"]}' # 2. 持久化接受退款请求;Agent 跑回合、查订单、调用 issue_refund —— 并停在审批处 curl -X POST 'http://localhost:8787/ops/request-refund?session=demo' \ -d '{"orderId":"ord_123","amountCents":4200,"reason":"late delivery"}' # 3. 注入带外上下文(模拟上游 "dispute escalated" webhook),不启动回合 curl -X POST 'http://localhost:8787/ops/inject-context?session=demo' \ -d '{"note":"customer opened a chargeback"}' # 4. 查询 submission / 导出 transcript + ledger + scopes curl 'http://localhost:8787/ops/inspect?session=demo&id=SUBMISSION_ID' curl 'http://localhost:8787/ops/debug?session=demo'

九、状态与后续:审批回路是"下一步"的闸门

findings 的收尾明确了原型的定位:headless 且通过类型检查,刻意不是打磨过的示例。把它硬化成可上线的examples/应用(UI + 审批解析演示)被推迟到 Actions 的审批解析路径存在之后——没有它,审批回路无法端到端完成,这本身就是 headline finding。当前仓库中 Actions RFC 的状态为"Steps 1–6 landed"(含durable-pause停泊存储、claim-by-delete 恢复、连接无关的续跑与 pending-retry lease),approveExecution/rejectExecution已能跨部署地解析durable-pause动作;原型当初缺失的"后端工作流干净地批准一个通用工具审批"这一能力,正是 RFC 提案验证后补齐的答案。

【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 5:57:38

SR-IOV实战指南:破解虚拟机I/O性能瓶颈,让网络速度接近物理机

搞虚拟化的朋友&#xff0c;应该都经历过这种场景&#xff1a;宿主机上跑了几台云主机&#xff0c;业务流量稍微上来一点&#xff0c;虚拟机里网卡先飙到100%中断&#xff0c;宿主机CPU被软中断打得嗷嗷叫&#xff0c;但物理链路明明还有大把带宽没用完。你说气不气人&#xff…

作者头像 李华
网站建设 2026/9/17 5:54:02

2026 SMT贴片选型核心:材料-工艺-设备动态匹配

1. 为什么2026年的SMT贴片加工选型&#xff0c;不能再照搬2023年的经验&#xff1f;我去年帮一家做智能穿戴设备的客户做产线升级&#xff0c;他们拿着2023年用得挺顺的那套SMT方案——两台国产中端贴片机传统锡膏印刷机AOI人工复判流程——直接套到2026年的新项目上。结果样机…

作者头像 李华
网站建设 2026/9/17 5:52:17

Windows终端美化实战:PowerShell与oh-my-posh高效配置指南

1. 项目概述&#xff1a;把终端从"能用"变成"好用"1.1 为什么要折腾终端美化很多人每天打开终端&#xff0c;面对的就是那个万年不变的白字黑底&#xff0c;路径一长就分不清当前在哪个目录&#xff0c;Git 分支全靠手敲git status去确认&#xff0c;命令跑…

作者头像 李华