@addyosmani:你知道刷新页面会让 AI 聊天在响应中途中断吗?@triggerdotdev 的新聊天代理解决了这个问题,使其能存活…

X AI KOLs Timeline 工具

摘要

Trigger.dev 的新聊天代理让开发者能够构建持久、有状态的 AI 聊天体验,这些体验在刷新和崩溃后依然存活,没有超时限制,并且可以在危险工具调用前暂停以请求许可。

你知道刷新页面会让 AI 聊天在响应中途中断吗?@triggerdotdev 的新聊天代理解决了这个问题,因此它能在崩溃、重新部署后继续存活,并且可以在工具执行危险操作前暂停征求许可。值得一看:https://t.co/ljbyVvqzYH https://t.co/8A6N5BYFCb
查看原文
查看缓存全文

缓存时间: 2026/08/10 17:34

你知道刷新页面会中断 AI 聊天的半途响应吗?@triggerdotdev 的新 chat agent 解决了这个问题——它能在崩溃、重新部署后继续运行,还可以在工具执行危险操作前暂停并请求许可。值得一看:https://t.co/ljbyVvqzYH https://t.co/8A6N5BYFCb


介绍 chat agent

来源:https://trigger.dev/changelog/chat-agent?utm_source=fnf&utm_medium=x&utm_campaign=august&utm_term=aosmani&utm_content=chat-agent-launch

Chat agent 是一种构建持久化 AI 聊天体验的方式,它运行在一台没有超时限制的机器上,并且能在刷新页面和崩溃后持续流式输出。没人打字时机器会休眠,然后从上次中断的地方恢复运行,你完全不需要管理任何状态。

这台机器在第一条消息到达时启动,运行时间取决于工作完成所需的时间,并在每轮对话之间将所有内容保存在内存中:你的变量、你的缓存、你四个问题之前生成的子代理。你将每一轮对话编写为一个 Trigger.dev 任务,接收消息并返回一个流,然后让 AI SDK 的 useChat 直接指向它。中间不需要 API 路由,整个后端就是这样的:

import { chat } from "@trigger.dev/sdk/ai";
import { streamText, stepCountIs } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

export const myChat = chat.agent({
  id: "my-chat",
  run: async ({ messages, signal }) => {
    return streamText({
      model: anthropic("claude-sonnet-4-5"),
      messages,
      abortSignal: signal,
      stopWhen: stepCountIs(15),
    });
  },
});

Arena 基于 chat.agent 构建了 Agent Mode(https://x.com/arena/status/2062565126600114484),并在生产环境中大规模运行。

“每段对话都有一台真实的机器,这让我们的持久化代理构建起来简单得多。内置的追踪和可观测性让查看和调试代理会话变得非常容易。”

Graham Tremper Arena

他们不是唯一的使用者。chat.agent 自 7 月 2 日起正式全面可用,6 月起已投入生产运行,到目前为止已经服务了数百万个会话和超过 84 年的计算时长。

为什么 chat.agent 更好

大多数聊天后端将工作与请求绑定在一起,而普通的 API 端点并不适合聊天代理(或者说任何代理,但那是另一个故事了)。请求/响应周期通常有超时限制,而且是无状态的。你必须通过将所有内容写入数据库、使用 Redis 来实现持久化流、将慢速工作排队到后台 worker 并自行协调来对抗这些限制。

如果你的模型响应时间超过了函数允许的运行时间,祝你好运。或者某个环节失败了,你想要重试。或者你想生成一个自主思考的子代理。或者你想关掉浏览器,几天后再回来。有状态的机器要合适得多:

一台真正的 Linux 机器(https://trigger.dev/changelog/chat-agent?utm_source=fnf&utm_medium=x&utm_campaign=august&utm_term=aosmani&utm_content=chat-agent-launch#a-real-linux-machine)。 想装什么就装什么,运行任何 CLI,自选 CPU 和内存。

持久化计算,持久化流(https://trigger.dev/changelog/chat-agent?utm_source=fnf&utm_medium=x&utm_campaign=august&utm_term=aosmani&utm_content=chat-agent-launch#durable-compute-durable-streams)。 单轮对话没有超时。一段对话可以关掉,几天后再回来继续。

快速的首轮响应(https://trigger.dev/changelog/chat-agent?utm_source=fnf&utm_medium=x&utm_campaign=august&utm_term=aosmani&utm_content=chat-agent-launch#fast-first-turns)。 首次 LLM 调用在你自己的热服务器上运行,同时代理并行启动,所以持久化不等于慢。

等待不花一分钱(https://trigger.dev/changelog/chat-agent?utm_source=fnf&utm_medium=x&utm_campaign=august&utm_term=aosmani&utm_content=chat-agent-launch#waiting-costs-nothing)。 代理可以停下来,等一个人批准某项操作,然后在那里挂机好几天而不产生任何费用。

内置追踪和指标(https://trigger.dev/changelog/chat-agent?utm_source=fnf&utm_medium=x&utm_campaign=august&utm_term=aosmani&utm_content=chat-agent-launch#tracing-and-metrics-built-in)。 每一轮对话都是一个 span,并且有一个 AI 指标仪表盘,展示成本、token 数和延迟。

而且它使用的是你可能已经在用的 AI SDK(https://trigger.dev/changelog/chat-agent?utm_source=fnf&utm_medium=x&utm_campaign=august&utm_term=aosmani&utm_content=chat-agent-launch#keep-using-the-ai-sdk)。

一台真正的 Linux 机器

对话背后的计算机是一台普通的 Linux 机器,而不是受限的运行时。想装什么就装什么。可以调用 ffmpeg、驱动无头浏览器、运行一个需要真实文件系统和真实进程树的 CLI。

import { tool } from "ai";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { z } from "zod";

const exec = promisify(execFile);

export const transcode = tool({
  description: "Transcode a video to MP4.",
  inputSchema: z.object({ input: z.string(), output: z.string() }),
  execute: async ({ input, output }) => {
    await exec("ffmpeg", ["-i", input, "-c:v", "libx264", output]);
    return { output };
  },
});

你可以选择机器的大小。机器预设(https://trigger.dev/docs/machines)从 micro(0.25 vCPU)起步,每台都有专属的 CPU、内存和磁盘。可以在代理级别设置,也可以在每个会话中覆盖。

持久化计算,持久化流

单轮对话没有超时限制。它运行多久取决于工作需要多久,所以一个慢速工具、一长串工具调用,或者一个自主思考的子代理(https://trigger.dev/docs/ai-chat/patterns/sub-agents)都只是工作而已。你不需要把任何东西切成适合限制的小块,也不需要为慢的部分排队。

在生产环境中,每二十个代理运行中就有一个持续超过 36 分钟,远远超出普通请求会被切断的时间点。

内存会在休眠后保留。你在第三轮对话中放在变量里的东西,明天第二十轮时仍然在那里,所以你之前做过的昂贵查找不需要重做。

const embeddings = new Map();

export const myChat = chat.agent({
  id: "my-chat",
  run: async ({ messages, signal }) => {
    const key = messages.at(-1)!.id;
    const cached = embeddings.get(key) ?? (await embed(key));
    embeddings.set(key, cached);
    return streamText({ model, messages, abortSignal: signal });
  },
});

如果机器崩溃了,你会得到一台新的,对话也会随之恢复,因为那部分是被持久化下来的。但堆内存不会:你的 Map 会重新从空开始。这和你运维过的任何长运行服务器的规则一样,所以把无法承受丢失的东西放进数据库,内存留给需要速度的地方。这里的不同之处在于,内存现在能持续整个对话,而不是单个请求。

流也是持久化的。一段对话是一个会话,而会话比服务于它的进程活得更久:run 是计算,session 是身份。在响应中途刷新页面,流会从你的浏览器停止读取的地方重放,而不会重新运行模型。关掉浏览器,几天后回来,对话仍然在那里。

这让故障场景变得平淡无奇。如果一个 run 被杀死或内存耗尽,下一条消息会启动一个新的 run 并恢复对话。部署不会中断任何东西:一个 run 会继续运行在它启动时的版本上,而将对话迁移到新代码是你自己决定并调用的(https://trigger.dev/docs/ai-chat/patterns/version-upgrades),使用 chat.requestUpgrade()。

快速的首轮响应

持久化不意味着慢。Head Start(https://trigger.dev/docs/ai-chat/fast-starts#head-start)是可选的,当你希望第一个 token 和普通端点一样快时可以使用它。它在你自己的热服务器上运行首次 LLM 调用,同时代理并行启动,然后在对话中途移交控制权,处理工具调用以及后续所有内容。用户看到的是一个连续的响应,完全感觉不到接缝。

import { chat } from "@trigger.dev/sdk/chat-server";
import { streamText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { headStartTools } from "@/lib/chat-tools/schemas";

export const chatHandler = chat.headStart({
  agentId: "my-chat",
  run: async ({ chat: helper }) =>
    streamText({
      ...helper.toStreamTextOptions({ tools: headStartTools }),
      model: anthropic("claude-sonnet-4-5"),
      system: "You are a helpful assistant.",
    }),
});

在我们的测试中,它大约将首 token 时间和整轮对话时长都缩短了一半。它返回一个普通的 Web Fetch handler,所以可以无需适配器直接挂载到 Next.js、Hono、SvelteKit、Remix、TanStack Start 等框架中。而当首轮是纯文本、没有工具调用时,代理启动后立即退出,从不调用模型,所以你只为对话实际需要的东西付费。

因为内存是快照而不是重放的,这一切都不会给你的代码强加确定性约束。没有需要重放的事件历史,所以 Date.now()、Math.random() 以及循环中间的 fetch 都只是普通代码。没什么需要做成确定性的,也没有重放日志需要做版本管理。

等待不花一分钱

没有 execute 函数的工具会在轮次结束时保持调用打开状态。

export const requestApproval = tool({
  description: "Ask the user to approve an irreversible action.",
  inputSchema: z.object({ action: z.string() }),
  // No execute. The turn ends here and the agent suspends.
});

代理会挂起,人想花多久就花多久,他们的回答会恢复运行。因为是挂起状态,你不会为等待付费,所以一个审批可以挂一晚上或者一个周末。

常用的聊天控制功能也都有。可以在流式生成中途停止(https://trigger.dev/docs/ai-chat/frontend#stop-generation),在工具调用之间引导它(https://trigger.dev/docs/ai-chat/pending-messages),或者编辑、分支和重新生成(https://trigger.dev/docs/ai-chat/patterns/branching-conversations),一路回溯到更早的消息。

内置追踪和指标

每一轮对话在仪表盘中都是一个 span,所以你可以打开一段对话,看到提示词、响应、工具调用以及每项花费的时间。

同一段对话中的两轮:模型调用、工具调用,以及代理挂起时等待的审批。

每个项目都自带一个 AI 指标 仪表盘。总花费、总调用次数、平均首块时间、平均每秒 token 数,以及按时间变化的成本、按模型划分的成本、按时间变化的 token 数、按模型划分的延迟百分位数、结束原因、最昂贵的运行,以及按任务和按提供商划分的成本。

项目中调用过的每个模型都有对应的花费、吞吐量和延迟数据,无需编写任何埋点代码。

每个模型都有自己的行,所以你可以看到每个模型实际花了你多少钱,以及提示词缓存帮你省了多少。成本、缓存节省、首块时间和吞吐量,按模型展示。

这些图表背后的数据可以通过 TRQL(https://trigger.dev/changelog/query-and-dashboards)获取,所以你可以自己提问、构建自己的仪表盘。sessions.list 给你所有对话,足以构建一个收件箱。

继续使用 AI SDK

服务端用 streamText,客户端用 useChat。chat.agent 作为传输层嵌入在底层,两者之间的 API 路由就不需要了。

import { useChat } from "@ai-sdk/react";
import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
import type { myChat } from "@/trigger/chat";
import { mintChatAccessToken, startChatSession } from "@/app/actions";

export function Chat() {
  const transport = useTriggerChatTransport({
    task: "my-chat",
    accessToken: ({ chatId }) => mintChatAccessToken(chatId),
    startSession: ({ chatId, clientData }) =>
      startChatSession({ chatId, clientData }),
  });

  const { messages, sendMessage } = useChat({ transport });
  // ... render UI
}

只有新消息会通过网络传输。历史记录累积在服务器上,所以你不需要每轮对话都重新上传整个对话内容。如果你已经有一个聊天应用,迁移指南(https://trigger.dev/docs/ai-chat/migrating-from-a-route-handler)里有一段提示词,你可以直接交给你的编码代理。

还有更多

你不必只停留在顶层。chat.agent() 为你管理轮次循环,自定义代理(https://trigger.dev/docs/ai-chat/custom-agents)会把控制权交还给你,让你自己编写循环,再往下还有底层会话原语(https://trigger.dev/docs/ai-chat/backend),让你直接读写对话的流。从简单的开始,需要时再往下深入。

长对话也能保持经济实惠:压缩(https://trigger.dev/docs/ai-chat/compaction)会在接近上下文限制时总结历史记录,提示词缓存(https://trigger.dev/docs/ai-chat/prompt-caching)让每个请求的稳定前缀保持低成本。你决定何时触发以及摘要长什么样。

export const myChat = chat.agent({
  id: "my-chat",
  compaction: {
    shouldCompact: ({ totalTokens }) => (totalTokens ?? 0) > 80_000,
    summarize: async ({ messages }) => summariseWithHaiku(messages),
  },
  run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }),
});

你的提示词(https://trigger.dev/docs/ai/prompts)也可以放在代码库中。它们会在每次部署时进行版本管理,你可以从仪表盘覆盖文本或模型而无需重新部署,每次生成都会追溯到产生它的版本。

固定在 v5 上的覆盖配置:线上调用会解析到那里,而不是部署所请求的版本。

浏览器不是唯一的客户端。AgentChat(https://trigger.dev/docs/ai-chat/server-chat)可以从一个任务、一个脚本或另一个代理驱动对话,MCP 服务器(https://trigger.dev/docs/ai-chat/mcp)让你无需编写任何代码就能从 Claude Code 或 Cursor 与自己的代理对话。

const review = new AgentChat({ agent: "my-chat", id: `review-${prNumber}` });

const summary = await (await review.sendMessage(`Review PR #${prNumber}`)).text();
await review.close();

前端还有 actions(https://trigger.dev/docs/ai-chat/actions),用于不消耗一轮对话的状态变更命令。当出错时,被内存不足错误杀死的轮次会在更大的机器上自动重试(https://trigger.dev/docs/ai-chat/patterns/oom-resilience)而不丢失导致错误的消息,恢复启动(https://trigger.dev/docs/ai-chat/patterns/recovery-boot)会在崩溃或取消后恢复完整上下文,版本升级(https://trigger.dev/docs/ai-chat/patterns/version-upgrades)在你决定时机时将挂起的对话迁移到新代码上。你还可以给代理赋予技能(https://trigger.dev/docs/ai-chat/patterns/skills),即一组它可以按需发现和使用的指令和脚本文件夹。

测试,以及 Trigger.dev 的其余部分

你可以在单元测试中通过真实的轮次来驱动代理,从而测试代理(https://trigger.dev/docs/ai-chat/testing)。不需要网络,不需要任务运行时,我们的任何东西都不会在你背后被 mock 掉。

因为代理就是一个 Trigger.dev 任务,所有现有功能也都适用。可以从工具调用中触发其他任务并等待它们完成,将聊天放在带独立并发控制的队列后面,批量处理工作,调度任务,或者构建自己的 eval 作为任务,在每次部署时针对一组固定数据运行代理。这些都不需要你专门去采用一个独立的产品。

开源,以及费用

Trigger.dev 使用 Apache 2.0 许可证,chat.agent 就是其中的一部分。阅读代码(https://github.com/triggerdotdev/trigger.dev)了解这一切是如何运作的,如果你愿意,也可以自己部署运行整套系统(https://trigger.dev/docs/open-source-self-hosting)。聊天代理的计费方式为 com

相似文章

Trigger.dev 的 Chat Agent

Product Hunt

Trigger.dev 推出了一款 Chat Agent,即使你关闭标签页,AI 对话也会持续运行。