Google 发布了面向 TypeScript 和 Go 的 Genkit Agents API 预览版。它把消息历史、工具调用循环、流式输出和状态持久化收拢到统一的 chat() 接口背后,同时加入两个面向生产场景的重要能力:客户端断开后仍能执行的 detached turns,以及支持人工介入和恢复校验的 interruptible tools。
这次变化的重点不是多了一层聊天封装,而是开始正面处理智能体应用中最棘手的生命周期问题:一次任务可能持续数分钟,用户未必一直在线;某些工具调用具有副作用,不能仅凭模型决定执行。
chat() 统一的不只是消息格式
普通聊天接口往往只负责接收消息和返回文本。智能体回合则可能经历多次模型推理和工具调用:
- 模型读取历史消息和当前状态。
- 模型决定调用搜索、数据库或业务 API。
- 工具返回结果,模型继续推理。
- 中间状态被保存,输出以流式或非流式方式返回。
Genkit Agents API 将这些机制包装在单一 chat() 接口后。对应用开发者而言,价值在于调用方不必自行拼接每轮工具结果、恢复消息历史或维护复杂的执行循环。
不过,统一接口并不意味着状态边界消失了。接入时仍应明确区分:
- 对话状态:用户消息、模型回复和工具结果。
- 业务状态:订单、工单、付款记录等系统事实。
- 执行状态:当前回合是否运行、暂停、完成或失败。
业务数据库仍应是真实数据的权威来源。不要因为框架能够持久化智能体状态,就把不可逆的业务结果只保存在对话记录里。
Detached turn 解决连接寿命与任务寿命不一致
HTTP 请求、浏览器标签页和移动网络连接都很短暂,但智能体任务可能需要等待多个外部系统。Detached turn 允许客户端断开后,回合继续执行。这适合报告生成、跨系统信息收集、批量检查和耗时工具调用。
它也改变了接口设计。调用方不应假设每个请求都会同步拿到最终答案,而应准备处理类似下面的状态机:
queued -> running -> waiting_for_human -> completed
\-> failed
可以这样实践:入口接口快速返回任务 ID,客户端通过轮询、Server-Sent Events 或其他通知机制获取进度。生产环境还需要持久化执行状态,并为重复提交设计幂等键。
POST /agent-runs HTTP/1.1
Content-Type: application/json
Idempotency-Key: report-user-42-2025-03-08
{
"agent": "account-review",
"message": "检查该账户并生成风险摘要"
}
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"runId": "run_01JXYZ",
"status": "queued",
"statusUrl": "/agent-runs/run_01JXYZ"
}
Detached 并不自动等于可靠执行。部署时仍需检查进程重启、超时、工具重试、重复消费和取消任务时的行为。如果任务必须跨进程存活,应使用框架支持的持久化能力,并结合部署环境中的可靠任务执行机制。
人工介入不是一个普通确认按钮
Interruptible tools 允许智能体在执行敏感工具前暂停,将控制权交给人类。例如,读取公开资料可以自动完成,但退款、发送邮件、修改权限或提交采购单应等待审批。
真正困难的部分发生在“恢复执行”时。攻击者可能篡改工具参数、伪造审批结果,或者重复提交已经使用过的恢复请求。摘要中特别提到恢复时的 anti-forgery validation,说明恢复凭证必须与原始暂停点绑定并经过验证。
下面是一个可运行的 TypeScript 小例子,用 HMAC 签名演示恢复令牌、参数绑定和防重放。它不是 Genkit 预览 API 的精确调用方式,而是可以嵌入实际审批端点的安全骨架;接入时应根据当前 Genkit 文档替换暂停和恢复部分。
运行前把 APPROVAL_SECRET 改成高强度随机值:
mkdir agent-approval-demo
cd agent-approval-demo
npm init -y
npm install -D typescript tsx @types/node
export APPROVAL_SECRET="replace-with-a-long-random-secret"
创建 index.ts:
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
const secret = process.env.APPROVAL_SECRET;
if (!secret) throw new Error("APPROVAL_SECRET is required");
const usedNonces = new Set<string>();
type Approval = {
runId: string;
tool: string;
args: Record<string, unknown>;
nonce: string;
expiresAt: number;
};
function encode(value: unknown): string {
return Buffer.from(JSON.stringify(value)).toString("base64url");
}
function sign(payload: string): string {
return createHmac("sha256", secret).update(payload).digest("base64url");
}
function issueApproval(data: Omit<Approval, "nonce" | "expiresAt">): string {
const approval: Approval = {
...data,
nonce: randomUUID(),
expiresAt: Date.now() + 5 * 60_000,
};
const payload = encode(approval);
return `${payload}.${sign(payload)}`;
}
function consumeApproval(token: string): Approval {
const [payload, suppliedSignature] = token.split(".");
if (!payload || !suppliedSignature) throw new Error("Malformed token");
const expected = Buffer.from(sign(payload));
const supplied = Buffer.from(suppliedSignature);
if (expected.length !== supplied.length || !timingSafeEqual(expected, supplied)) {
throw new Error("Invalid signature");
}
const approval = JSON.parse(
Buffer.from(payload, "base64url").toString("utf8"),
) as Approval;
if (approval.expiresAt < Date.now()) throw new Error("Approval expired");
if (usedNonces.has(approval.nonce)) throw new Error("Approval already used");
usedNonces.add(approval.nonce);
return approval;
}
const token = issueApproval({
runId: "run-123",
tool: "issueRefund",
args: { orderId: "order-9", amount: 49.9 },
});
console.log("Send this token to the approval UI:", token);
const approvedCall = consumeApproval(token);
console.log("Verified tool call:", approvedCall);
try {
consumeApproval(token);
} catch (error) {
console.error("Replay blocked:", (error as Error).message);
}
执行:
npx tsx index.ts
真实系统还应在恢复时从服务端存储读取原始工具调用,比较 runId、工具名和参数摘要,而不是信任浏览器回传的参数。示例中的内存 Set 也必须替换为数据库或共享缓存,否则多实例部署和进程重启会使防重放失效。
接入时关注生命周期,而不只是模型效果
Genkit Agents API 仍处于预览阶段,接口和行为可能变化。适合先在内部工具或低风险流程中验证,再逐步扩大范围。评估时可以使用这份清单:
- 为每个 detached turn 设置超时、取消和幂等策略。
- 明确哪些工具可自动执行,哪些必须人工批准。
- 将恢复令牌绑定到运行 ID、工具调用、参数和过期时间。
- 记录暂停、批准、拒绝、恢复和最终执行结果,形成审计链。
- 测试客户端断开、服务重启、工具超时和重复恢复请求。
- 不把对话状态当成业务数据库,也不让模型直接决定不可逆操作。
统一的 chat() 接口降低了构建工具型智能体的代码复杂度,而 detached turns 和 interruptible tools 补上了长期任务与人工控制这两个关键环节。真正决定系统能否上线的,仍然是持久化、一致性、鉴权和审计这些工程边界。