<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Niapya's Blog</title>
        <link>https://niapya.github.io</link>
        <description>这里是 Niapya's Blog。</description>
        <lastBuildDate>Mon, 01 Jun 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Niapya's Blog</generator>
        <language>zh-CN</language>
        <atom:link href="https://niapya.github.io/rss.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[如何构建一个无服务器 Agent]]></title>
            <link>https://niapya.github.io/posts/how-to-build-a-serverless-agent</link>
            <guid isPermaLink="false">https://niapya.github.io/posts/how-to-build-a-serverless-agent</guid>
            <pubDate>Mon, 01 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[4 月初，我们开源了 Serverless 项目 ClawLess，它可以在你的 Vercel 账户上免费部署一个 24 小时可用的轻量 Agent。
这篇文章想记录的是 ClawLess 背后的取舍：为什么它应该是 Serverless 的，以及我们如何用 Vercel Workflow、Vercel AI SDK、Vercel Sandbox、KV、Blob 和 Postgres Vector]]></description>
            <content:encoded><![CDATA[<p>4 月初，我们开源了 Serverless 项目 <a href="https://github.com/Niapya/clawless">ClawLess</a>，它可以在你的 Vercel 账户上免费部署一个 24 小时可用的轻量 Agent。</p>
<p>这篇文章想记录的是 ClawLess 背后的取舍：为什么它应该是 Serverless 的，以及我们如何用 Vercel Workflow、Vercel AI SDK、Vercel Sandbox、KV、Blob 和 Postgres Vector 组装出一个能工作的 Agent。</p>
<h1 id="起因">起因</h1>
<p><strong>构建一个完整、长期在线的 Agent 运行时，成本其实不在模型，而在一直运行。</strong></p>
<p>模型你可以选择低价或者免费的模型，但你仍然需要一个自己的服务器、VPS，或者一台长期在线的 Mac mini。</p>
<p>有些项目已经提出了另一种解决方案：使用更小的原生语言重写运行时，把内存占用压低，让它可以运行在树莓派、开发板，或者更便宜的 Linux 设备上。</p>
<p>他们至少需要一个长期在线的设备来运行 Agent，维护它的状态，等待用户消息，处理定时任务，而 ClawLess 一开始就不是这个方向。</p>
<p>对大多数人来说，Agent 最常见的入口仍然是 chatbot：用户发送消息，Agent 读取上下文、调用工具、返回结果。它不需要每一秒都在 CPU 上运行，它只需要在被唤醒时可靠地处理请求。</p>
<p>我们只需要在它接收到会话的时候处理请求即可：</p>
<ul>
<li>如果是在网页上，那就是在用户发送消息时触发的 API 请求。</li>
<li>如果连接你的 IM，那就是 IM 在收到请求时触发的 webhook，也可以是 API 请求。</li>
</ul>
<p>除此之外，我们还需要定时任务和延迟任务。比如「明天提醒我继续这个话题」，或者「每天早上把某个 channel 的信息整理一下」。这类任务本质上不要求常驻进程，只要求平台能把某个 workflow 暂停、等待，并在合适的时间继续。</p>
<p>老朋友都知道我比较喜欢 Serverless。它的核心是请求进来时启动，任务挂起时休眠，时间到了再恢复。</p>
<p>由于这种特性，Serverless 服务平台可以以非常低的价格部署，特别是对于 JS Rutime 的，比如 AWS Lambda、Vercel、Cloudflare Workers，而且很多平台都有足够尝鲜的 free tier。</p>
<p>经过调研，我看上了 Vercel Workflow。它可以启动长时间任务，可以在 Workflow 里等待，也可以在特定时间继续执行，因此 ClawLess 可以被消息、Webhook、工具审批和 schedule 唤醒的 Workflow。</p>
<p>换句话说，ClawLess 并不需要你一直运行，只需要按需唤醒。这恰好是 Workflow 这类运行时擅长的事情。</p>
<h1 id="设计">设计</h1>
<p>因为同时有前后端，这是一个全栈项目。</p>
<p>对于全栈，我们曾尝试过使用 Nuxt，或是 Hono SSR 的方式来构建全栈，但我们采用 Next.js 来作为全栈框架，因为我们要使用 Vercel Workflow 的 API 来构建 Agent，而 Next.js 是 Vercel Workflow 的原生支持框架。</p>
<h2 id="workflow-是-agent-的运行时">Workflow 是 Agent 的运行时</h2>
<p>我们先谈一下 Vercel Workflow，它是 Vercel 最近推出的官方、长时间运行的、带有状态的流程运行框架。经测试，一个 workflow 能运行至少 10 分钟以上。</p>
<blockquote>
<p>我们在做 ClawLess 的时候，Vercel Workflow 还是处于 Beta 阶段，但是现在 API 已经稳定。</p>
</blockquote>
<p>它使用 <code>&quot;use workflow&quot;;</code> 和 <code>&quot;use step&quot;;</code> 指令来定义一个 workflow。</p>
<ul>
<li><code>use workflow</code> 负责“编排”，也就是跨步骤的状态、等待、恢复和整体流程。</li>
<li><code>use step</code> 负责“执行”，也就是可以单独重试、适合封装副作用的原子操作。</li>
</ul>
<p>对于 Agent 来说，真正调用数据库、第三方 API、Sandbox 命令的逻辑，通常放在到 <code>step</code> 里；而等待用户消息、等待审批，比如等待明天早上 9 点继续执行的逻辑，则更适合留在 <code>workflow</code> 这一层。</p>
<p>一个 Workflow 的例子如下：</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">updateUser</span>(<span class="hljs-params"><span class="hljs-attr">userId</span>: <span class="hljs-built_in">string</span></span>) {
	<span class="hljs-string">&quot;use step&quot;</span>;
	<span class="hljs-keyword">await</span> db.<span class="hljs-title function_">insert</span>({ <span class="hljs-attr">id</span>: userId });
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">userOnboardingWorkflow</span>(<span class="hljs-params"><span class="hljs-attr">userId</span>: <span class="hljs-built_in">string</span></span>) {
  <span class="hljs-string">&quot;use workflow&quot;</span>;
  <span class="hljs-keyword">await</span> <span class="hljs-title function_">updateUser</span>(userId);
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">POST</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">await</span> <span class="hljs-title function_">updateUser</span>(<span class="hljs-string">&quot;123&quot;</span>);
}
</code></pre>
<p>然后对于 AI 来说，毫无疑问，TypeScript 最流行的 AI framework 是 Vercel AI SDK。</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-keyword">import</span> {
	convertToModelMessages,
	streamText,
	<span class="hljs-keyword">type</span> <span class="hljs-title class_">UIMessage</span>,
} <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;ai&quot;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">POST</span>(<span class="hljs-params"><span class="hljs-attr">req</span>: <span class="hljs-title class_">Request</span></span>) {
	<span class="hljs-keyword">const</span> { messages }: { <span class="hljs-attr">messages</span>: <span class="hljs-title class_">UIMessage</span>[] } = <span class="hljs-keyword">await</span> req.<span class="hljs-title function_">json</span>();

	<span class="hljs-keyword">const</span> result = <span class="hljs-title function_">streamText</span>({
		<span class="hljs-attr">model</span>: <span class="hljs-string">&quot;anthropic/claude-sonnet-4.5&quot;</span>,
		<span class="hljs-attr">system</span>: <span class="hljs-string">&quot;You are a helpful assistant.&quot;</span>,
		<span class="hljs-attr">messages</span>: <span class="hljs-keyword">await</span> <span class="hljs-title function_">convertToModelMessages</span>(messages),
	});

	<span class="hljs-keyword">return</span> result.<span class="hljs-title function_">toUIMessageStreamResponse</span>();
}
</code></pre>
<p>然后在前端，你可以使用 <a href="https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat"><code>useChat</code></a> 这个 Hook 来轻松地实现一个聊天界面。</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-string">&#x27;use client&#x27;</span>;

<span class="hljs-keyword">import</span> { useChat } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@ai-sdk/react&#x27;</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">DefaultChatTransport</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;ai&#x27;</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;react&#x27;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">Page</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">const</span> { messages, sendMessage, status } = <span class="hljs-title function_">useChat</span>({
    <span class="hljs-attr">transport</span>: <span class="hljs-keyword">new</span> <span class="hljs-title class_">DefaultChatTransport</span>({
      <span class="hljs-attr">api</span>: <span class="hljs-string">&#x27;/api/chat&#x27;</span>,
    }),
  });
  <span class="hljs-keyword">const</span> [input, setInput] = <span class="hljs-title function_">useState</span>(<span class="hljs-string">&#x27;&#x27;</span>);

  <span class="hljs-keyword">return</span> (
    <span class="language-xml"><span class="hljs-tag">&lt;&gt;</span>
      {messages.map(message =&gt; (
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{message.id}</span>&gt;</span>
          {message.role === &#x27;user&#x27; ? &#x27;User: &#x27; : &#x27;AI: &#x27;}
          {message.parts.map((part, index) =&gt;
            part.type === &#x27;text&#x27; ? <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{index}</span>&gt;</span>{part.text}<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span> : null,
          )}
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      ))}

      <span class="hljs-tag">&lt;<span class="hljs-name">form</span>
        <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{e</span> =&gt;</span> {
          e.preventDefault();
          if (input.trim()) {
            sendMessage({ text: input });
            setInput(&#x27;&#x27;);
          }
        }}
      &gt;
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
          <span class="hljs-attr">value</span>=<span class="hljs-string">{input}</span>
          <span class="hljs-attr">onChange</span>=<span class="hljs-string">{e</span> =&gt;</span> setInput(e.target.value)}
          disabled={status !== &#x27;ready&#x27;}
          placeholder=&quot;Say something...&quot;
        /&gt;
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;submit&quot;</span> <span class="hljs-attr">disabled</span>=<span class="hljs-string">{status</span> !== <span class="hljs-string">&#x27;ready&#x27;</span>}&gt;</span>
          Submit
        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
    <span class="hljs-tag">&lt;/&gt;</span></span>
  );
}
</code></pre>
<blockquote>
<p><code>useChat</code> Hooks 还提供了 Vue, Svelte 和 Angular 的库。</p>
</blockquote>
<p>这已经能完成一个很好用的 Web Chat。但它还不是我们要的 Durable Agent，因为请求中断/结束之后，运行时也就跟着结束了。</p>
<p>这与我们 Agent 的逻辑不同，我们希望在请求中断后还能继续运行，并且在运行后持久化以保存状态。</p>
<p>所以我们的 Workflow 配备了 AI 模块，它和 AI SDK 深度集成，并提供了更适合 workflow 场景的 <code>DurableAgent</code>。</p>
<p>Workflow 的 <code>getWritable()</code> 可以拿到一个持久化的输出流，客户端断开以后还能重新连接回来，继续接收之前没看完的内容。</p>
<p>下面这段是一个简化示意：</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-keyword">import</span> { <span class="hljs-title class_">DurableAgent</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@workflow/ai/agent&quot;</span>;
<span class="hljs-keyword">import</span> { getWritable } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;workflow&quot;</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { <span class="hljs-title class_">UIMessage</span>, <span class="hljs-title class_">UIMessageChunk</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;ai&quot;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">chatWorkflow</span>(<span class="hljs-params"><span class="hljs-attr">messages</span>: <span class="hljs-title class_">UIMessage</span>[]</span>) {
	<span class="hljs-string">&quot;use workflow&quot;</span>;

	<span class="hljs-keyword">const</span> writable = getWritable&lt;<span class="hljs-title class_">UIMessageChunk</span>&gt;();
	<span class="hljs-keyword">const</span> agent = <span class="hljs-keyword">new</span> <span class="hljs-title class_">DurableAgent</span>({
		<span class="hljs-attr">model</span>: <span class="hljs-string">&quot;anthropic/claude-sonnet-4.5&quot;</span>,
		<span class="hljs-attr">system</span>: <span class="hljs-string">&quot;You are a helpful assistant.&quot;</span>,
	});

	<span class="hljs-keyword">await</span> agent.<span class="hljs-title function_">stream</span>({
		messages,
		writable,
	});
}
</code></pre>
<p>这时候，Agent 才真正开始像一个自主的系统。</p>
<blockquote>
<p>在 ClawLess 中，我们的过程更加复杂，主 workflow 会从 KV 读取当前配置，构建 system prompt，从 MCP 加载工具，创建 <code>DurableAgent</code>，然后把输出写到 UI message stream，并且持久化，最后再把状态保存到 Postgress SQL 中。</p>
</blockquote>
<h2 id="hook-让-workflow-真正可交互">Hook 让 workflow 真正可交互</h2>
<p>考虑更多，比如用户在网页继续发消息，或者 IM webhook 继续发消息时，如果 workflow 还在运行，我们应该去引导（Follow-up）Agent 处理这些消息，而不是直接发起新的 workflow。</p>
<blockquote>
<p>这是 Human in the Loop 的基本逻辑。如果你想了解更多，你可以看 WikiPedia 中的 <a href="https://en.wikipedia.org/wiki/Human-in-the-loop">HITL, Human-in-the-Loop</a>.</p>
</blockquote>
<p>庆幸的是，Workflow 提供了 hook 来做到这些。hook 的价值是：它允许一个正在运行的 workflow 暂停在某个位置，等外部事件到来之后再继续。</p>
<p>最简单的外部事件就是“下一条用户消息”：</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-keyword">import</span> { defineHook } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;workflow&quot;</span>;

<span class="hljs-comment">// 定义一个 hook，用于接收外部事件（下一条用户消息）</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> messageHook = defineHook&lt;{
	<span class="hljs-attr">text</span>: <span class="hljs-built_in">string</span>;
	<span class="hljs-attr">userId</span>: <span class="hljs-built_in">string</span>;
}&gt;();

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">conversation</span>(<span class="hljs-params"><span class="hljs-attr">sessionId</span>: <span class="hljs-built_in">string</span></span>) {
	<span class="hljs-string">&quot;use workflow&quot;</span>;

	<span class="hljs-keyword">const</span> events = messageHook.<span class="hljs-title function_">create</span>({
		<span class="hljs-attr">token</span>: <span class="hljs-string">&quot;chat-&quot;</span> + sessionId,
	});

	<span class="hljs-comment">// 等待外部事件（下一条用户消息）</span>
	<span class="hljs-keyword">for</span> <span class="hljs-title function_">await</span> (<span class="hljs-keyword">const</span> event <span class="hljs-keyword">of</span> events) {
		<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">&quot;Received&quot;</span>, event.<span class="hljs-property">text</span>, <span class="hljs-string">&quot;from&quot;</span>, event.<span class="hljs-property">userId</span>);
	}
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">POST</span>(<span class="hljs-params"><span class="hljs-attr">req</span>: <span class="hljs-title class_">Request</span></span>) {
	<span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> req.<span class="hljs-title function_">json</span>();

	<span class="hljs-keyword">await</span> messageHook.<span class="hljs-title function_">resume</span>(<span class="hljs-string">&quot;chat-&quot;</span> + data.<span class="hljs-property">sessionId</span>, {
		<span class="hljs-attr">text</span>: data.<span class="hljs-property">text</span>,
		<span class="hljs-attr">userId</span>: data.<span class="hljs-property">userId</span>,
	});

	<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Response</span>(<span class="hljs-string">&quot;OK&quot;</span>);
}
</code></pre>
<p>同一个机制也很适合做工具审批。比如模型想执行一个高风险命令：删文件、发消息、下单、转账，这时候就不该让它直接运行，而是要把审批请求抛给前端，再等用户确认。</p>
<blockquote>
<p>更多信息详见 AI SDK 中关于 <a href="https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling#tool-execution-approval">Tool Execution Approval</a> 的说明。</p>
</blockquote>
<p>Workflow hook 的好处是，你不需要自己轮询数据库，不需要写一套复杂的状态机，因为 workflow 会在等待时挂起，恢复时又从原来的状态继续。</p>
<p>所以，我们总结一下一个 Agent 的顺序：</p>
<ol>
<li>用户消息进入入口层。</li>
<li>入口层判断当前 session 是否已有活动中的 workflow。</li>
<li>如果有，就用 hook 把新消息送回去。</li>
<li>如果没有，就创建新的 workflow，从头开始处理。</li>
</ol>
<h2 id="上下文-context">上下文 Context</h2>
<p>Agent 有上下文限制（Context Limit）。不同模型的 token 计算方式不同，但它们都有一个共同限制：你必须把 system prompt、历史消息、工具结果、文件输入和记忆都塞进上下文里。</p>
<p>不仅如此，模型在处理长上下文时，正确率也会下降，成本也会增加。</p>
<p>处理长上下文有常见的两种方法，压缩和剪枝。</p>
<p>压缩（Compression）是把“很长但以后还需要”的历史，改写成更短的表示。最常见的做法是：在会话进行到一定长度之后，用模型把早期对话、关键工具结果、已经确认的用户偏好压成一段摘要，然后把原始消息移出当前上下文窗口。</p>
<p>下一轮真正调用模型时，你只需要把摘要重新组装回来。</p>
<blockquote>
<p>如果你考虑更多，你可以采用滚动窗口的方法，把摘要和最近几轮的原始消息以及当前输入加进来。</p>
</blockquote>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-keyword">import</span> { generateText, <span class="hljs-keyword">type</span> <span class="hljs-title class_">ModelMessage</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;ai&quot;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">compactMessages</span>(<span class="hljs-params"><span class="hljs-attr">messages</span>: <span class="hljs-title class_">ModelMessage</span>[]</span>) {
	<span class="hljs-keyword">const</span> summary = <span class="hljs-keyword">await</span> <span class="hljs-title function_">generateText</span>({
		model,
		<span class="hljs-attr">messages</span>: [
			...messages,
			{
				<span class="hljs-attr">role</span>: <span class="hljs-string">&quot;user&quot;</span>,
				<span class="hljs-attr">content</span>: <span class="hljs-string">&quot;请总结一下我们之前的对话，提炼出对后续有帮助的关键信息。&quot;</span>
			}
		]
	});

	<span class="hljs-keyword">return</span> {
		<span class="hljs-attr">summary</span>: summary.<span class="hljs-property">text</span>,
		<span class="hljs-attr">recentMessages</span>: messages.<span class="hljs-title function_">slice</span>(-<span class="hljs-number">8</span>),
	};
}
</code></pre>
<p>在 Workflow 里，这种做法尤其自然。因为 workflow 本来就有自己的生命周期和外部存储层，你完全可以在某一轮结束后把摘要写入 KV，把旧消息删掉，只保留一个“会话摘要”字段。这样 durable session 仍然存在，但真正送进模型的上下文不会无限增长。</p>
<p>另一种方法是剪枝（Pruning）。它不试图理解历史，而是直接把“对当前轮次帮助不大”的内容裁掉。</p>
<p>AI SDK 已经提供了 <code>pruneMessages</code>，可以在送进模型之前移除旧的 reasoning、工具调用结果、审批痕迹和空消息。</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-keyword">import</span> { pruneMessages, <span class="hljs-keyword">type</span> <span class="hljs-title class_">ModelMessage</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;ai&quot;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">buildContextWindow</span>(<span class="hljs-params"><span class="hljs-attr">messages</span>: <span class="hljs-title class_">ModelMessage</span>[]</span>) {
	<span class="hljs-keyword">const</span> pruned = <span class="hljs-title function_">pruneMessages</span>({
		messages,
		<span class="hljs-attr">reasoning</span>: <span class="hljs-string">&quot;before-last-message&quot;</span>,
		<span class="hljs-attr">toolCalls</span>: <span class="hljs-string">&quot;before-last-2-messages&quot;</span>,
		<span class="hljs-attr">emptyMessages</span>: <span class="hljs-string">&quot;remove&quot;</span>,
	});

	<span class="hljs-keyword">const</span> pinned = pruned.<span class="hljs-title function_">filter</span>(<span class="hljs-function">(<span class="hljs-params">message</span>) =&gt;</span> message.<span class="hljs-property">role</span> === <span class="hljs-string">&quot;system&quot;</span>);
	<span class="hljs-keyword">const</span> recent = pruned.<span class="hljs-title function_">filter</span>(<span class="hljs-function">(<span class="hljs-params">message</span>) =&gt;</span> message.<span class="hljs-property">role</span> !== <span class="hljs-string">&quot;system&quot;</span>).<span class="hljs-title function_">slice</span>(-<span class="hljs-number">12</span>);

	<span class="hljs-keyword">return</span> [...pinned, ...recent];
}
</code></pre>
<p>对于 Workflow AI，通常会在每一轮调用前通过 <code>prepareStep</code> 来重写。</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> agent.<span class="hljs-title function_">stream</span>({
	messages,
	writable,

	<span class="hljs-comment">// prepareStep hook 会在每次调用模型前被调用，用来重写上下文窗口</span>
	<span class="hljs-attr">prepareStep</span>: <span class="hljs-title function_">async</span> ({ <span class="hljs-attr">messages</span>: currentMessages }) =&gt; {
		<span class="hljs-keyword">return</span> {
			<span class="hljs-attr">messages</span>: <span class="hljs-title function_">buildContextWindow</span>(currentMessages),
		};
	},
});
</code></pre>
<p>这两种方法并不冲突。实际工程里更常见的是组合使用，完全取决于设计。</p>
<blockquote>
<p>这是有关 Context Engineering 的概念，你可以阅读 Anthropic 的文章 <a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents">Effective Context Engineering for AI Agents</a>.</p>
</blockquote>
<p>但是，我们还要要考虑的更多，比如说跨对话的请求之间的，同一知识之间的，或者是永久保存的上下文共享，这个时候我们需要引入工具来把它们存储在外部系统里，而不是直接放在模型上下文里。</p>
<h2 id="工具-tools">工具 Tools</h2>
<p><strong>Agent 的工具决定了它的能力边界。</strong></p>
<p>如果没有工具，它只是一个聊天模型；有了工具，它才可以保存记忆、执行命令、学习技能、安排任务，甚至把一部分工作交给子 Agent。</p>
<p>在 AI SDK 里，工具本身就是一等公民。一个最小的工具定义大概像这样：</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-keyword">import</span> { tool } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;ai&quot;</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;zod&quot;</span>;

<span class="hljs-keyword">const</span> tools = {
	<span class="hljs-attr">remember</span>: <span class="hljs-title function_">tool</span>({
		<span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Save a useful fact into long-term memory&quot;</span>,
		<span class="hljs-attr">inputSchema</span>: z.<span class="hljs-title function_">object</span>({
			<span class="hljs-attr">note</span>: z.<span class="hljs-title function_">string</span>(),
		}),
		<span class="hljs-attr">execute</span>: <span class="hljs-title function_">async</span> ({ note }) =&gt; {
			<span class="hljs-keyword">const</span> memory = <span class="hljs-keyword">await</span> <span class="hljs-title function_">buildMemory</span>(note);
			<span class="hljs-keyword">return</span> { <span class="hljs-attr">ok</span>: <span class="hljs-literal">true</span>, memory };
		},
	}),
};
</code></pre>
<h3 id="记忆-memory">记忆 Memory</h3>
<p>记忆是存储长期事实，用户偏好的部分。</p>
<p>你可以把它存储到 KV、数据库，或者以文件形式存储到 Blob，然后给模型添加工具来管理它。</p>
<p>在搜索时，最方便的时全文关键词搜索，但是有很多局限性，比如两个句话关联性强但是没有重叠。</p>
<p>你在公司内部系统搜索“如何处理产假期间的奖金核算”。问题：如果公司制度里使用的是“生育津贴”、“女职工福利”或“长期病假（生育）规定”，而没有同时包含“产假”和“奖金”这两个具体词汇，全文搜索会直接显示“未找到相关结果”，即便系统里实际上有相关的政策文档。</p>
<p>所以，我们可以考虑 <a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation">RAG, Retrieval-augmented generation, 检索增强生成</a>，这是一种让模型在生成响应时，从外部知识库中检索相关信息的方法。</p>
<p>最简单的是向量嵌入搜索，把一句话通过向量模型映射到一个多维向量上，然后在向量空间中查找最相似的句子。</p>
<p>在查找最相似时，可以使用余弦相似度（Cosine Similarity）的计算方法。</p>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mtext>Similarity</mtext><mo>=</mo><mover accent="true"><mi mathvariant="bold">q</mi><mo>^</mo></mover><mo>⋅</mo><mover accent="true"><mi mathvariant="bold">d</mi><mo>^</mo></mover><mo>=</mo><munderover><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>n</mi></munderover><msub><mover accent="true"><mi>q</mi><mo>^</mo></mover><mi>i</mi></msub><msub><mover accent="true"><mi>d</mi><mo>^</mo></mover><mi>i</mi></msub></mrow><annotation encoding="application/x-tex">\text{Similarity}
=
\hat{\mathbf{q}}
\cdot
\hat{\mathbf{d}}
=
\sum_{i=1}^{n}
\hat q_i \hat d_i</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord text"><span class="mord">Similarity</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.9023em;vertical-align:-0.1944em;"></span><span class="mord accent"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.7079em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord mathbf">q</span></span><span style="top:-3.0134em;"><span class="pstrut" style="height:3em;"></span><span class="accent-body" style="left:-0.25em;"><span class="mord">^</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.1944em;"><span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.9579em;"></span><span class="mord accent"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.9579em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord mathbf">d</span></span><span style="top:-3.2634em;"><span class="pstrut" style="height:3em;"></span><span class="accent-body" style="left:-0.25em;"><span class="mord">^</span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.9291em;vertical-align:-1.2777em;"></span><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.6514em;"><span style="top:-1.8723em;margin-left:0em;"><span class="pstrut" style="height:3.05em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mrel mtight">=</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.05em;"><span class="pstrut" style="height:3.05em;"></span><span><span class="mop op-symbol large-op">∑</span></span></span><span style="top:-4.3em;margin-left:0em;"><span class="pstrut" style="height:3.05em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.2777em;"><span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord"><span class="mord accent"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.6944em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord mathnormal" style="margin-right:0.0359em;">q</span></span><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="accent-body" style="left:-0.1667em;"><span class="mord">^</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.1944em;"><span></span></span></span></span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em;"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord"><span class="mord accent"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.9579em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord mathnormal">d</span></span><span style="top:-3.2634em;"><span class="pstrut" style="height:3em;"></span><span class="accent-body" style="left:-0.0833em;"><span class="mord">^</span></span></span></span></span></span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span></span>
<p>将计算出来相似度归一化，之后排序。</p>
<p>在真实系统中，我们一般用混合搜索，也就是将向量搜索和关键词搜索结合起来。</p>
<blockquote>
<p>更多关于 Memory 的探究，你可以参考 <a href="https://docs.openclaw.ai/concepts/memory">OpenClaw 的 Memory 设计</a>.</p>
</blockquote>
<h3 id="技能-skills">技能 Skills</h3>
<p>技能更像基于文件系统的知识库。</p>
<p>一个 Skill 一般存储在文件系统，它目录结构可能是这样的：</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="bash"><code class="language-bash">my-skill $ tree

my-skill/
	SKILL.md

	scripts/
	templates/
	examples/
	assets/
</code></pre>
<p>Skills 表明了怎么做事，比如：</p>
<ul>
<li>如何写某类测试</li>
<li>如何调用某个内部规范</li>
<li>如何按照团队约定生成代码</li>
</ul>
<p>我们一般会把 Skills 的标题和描述提取出来，注入到 System Prompt 中。</p>
<blockquote>
<p>Skills 是 Anthropic 提出的，具体请看 <a href="https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview">Agent Skills 概述</a>.</p>
</blockquote>
<h3 id="执行-bash">执行 Bash</h3>
<p>执行工具是 Agent 真正开始接触外部世界的地方。</p>
<p>一个最基础的执行工具就是 Bash 工具，他可以执行 Shell 命令，它允许了 Agent 和真实环境的连接起来。</p>
<blockquote>
<p>在 ClawLess 中，我们使用 @vercel/sandbox 来实现 Bash 工具。</p>
</blockquote>
<p>Bash 风险最高，也最需要隔离，要特别考虑工具审核和沙箱环境的隔离。</p>
<blockquote>
<p>如果你在寻找 ServerLess 的 Bash 工具或者是需要一个沙箱 Bash 工具，你可以考虑看看我们的 <a href="https://github.com/Niapya/ai-sdk-x">AI SDK X</a>.</p>
</blockquote>
<h3 id="任务">任务</h3>
<p>在处理复杂的任务时，我们还需要考虑更多。</p>
<h4 id="延时与定时任务">延时与定时任务</h4>
<p>延时任务会在一定时间之后提醒你。</p>
<p>ClawLess 在 Workflow 内部使用 <code>sleep</code> 函数来实现延时任务。</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-keyword">import</span> { sleep } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;workflow&quot;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">remindTomorrow</span>(<span class="hljs-params"><span class="hljs-attr">taskId</span>: <span class="hljs-built_in">string</span></span>) {
	<span class="hljs-string">&quot;use workflow&quot;</span>;

	<span class="hljs-keyword">await</span> <span class="hljs-title function_">sleep</span>(<span class="hljs-string">&quot;1 day&quot;</span>);
	<span class="hljs-keyword">await</span> <span class="hljs-title function_">sendReminder</span>(taskId);
}
</code></pre>
<p>定时任务又称作 Cron Job，它可以让你在指定的时间点执行任务。</p>
<p>定时任务的实现与延时任务相似，我们使用一个 <code>while (true)</code> 循环来实现定时任务。</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
	<span class="hljs-keyword">await</span> <span class="hljs-title function_">sleep</span>(<span class="hljs-string">&quot;1 day&quot;</span>);
	<span class="hljs-title function_">runTask</span>();
}
</code></pre>
<h4 id="子任务-sub-agent">子任务 Sub-Agent</h4>
<p>子任务不一定非要是另一个完整 Agent，他会把一部分小的任务委托出去。</p>
<p>例如主 Agent 负责和用户对话，但某个研究问题可以单独交给一个更便宜的小模型。</p>
<p>一个最小的 Sub-Agent 实现可以是这样的。</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-keyword">import</span> { generateText, tool } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;ai&quot;</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;zod&quot;</span>;

<span class="hljs-keyword">const</span> delegateResearch = <span class="hljs-title function_">tool</span>({
	<span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Hand off a focused research subtask&quot;</span>,
	<span class="hljs-attr">inputSchema</span>: z.<span class="hljs-title function_">object</span>({
		<span class="hljs-attr">question</span>: z.<span class="hljs-title function_">string</span>(),
	}),
	<span class="hljs-attr">execute</span>: <span class="hljs-title function_">async</span> ({ question }) =&gt; {
		<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> <span class="hljs-title function_">generateText</span>({
			<span class="hljs-attr">model</span>: <span class="hljs-string">&quot;openai/gpt-4.1-mini&quot;</span>,
			<span class="hljs-attr">prompt</span>: <span class="hljs-string">&quot;Answer briefly and return only key facts:
&quot;</span> + question,
		});

		<span class="hljs-keyword">return</span> { <span class="hljs-attr">summary</span>: result.<span class="hljs-property">text</span> };
	},
});
</code></pre>
<h3 id="mcp-与其他工具">MCP 与其他工具</h3>
<p>模型上下文协议 (MCP, Model Context Protocol) 是一个统一的协议，他让 Agent 统一接入外部系统的能力。</p>
<p>如果你在做一个特定领域的 Agent，往往更适合 MCP，而不是一堆 Skills。</p>
<p>ClawLess 其实并没有做网页搜索（Web Search），浏览器访问（Browser User）等功能，我们可以通过 MCP 来接入这些能力。</p>
<h2 id="与前端连接">与前端连接</h2>
<p>与 Agent 交互的前端部分，我们可以通过网页，Bot 或是其他地方与 Agent 链接。</p>
<h3 id="网页对话">网页对话</h3>
<p>我们刚才的 <code>useChat</code> Hooks 就是最简单的网页对话部分，我们在这里不再叙述。</p>
<h3 id="通过-webhook-与-im-连接">通过 Webhook 与 IM 连接</h3>
<p>大部分即时通讯软件一般会有内置 Bot 的能力，例如 Telegram 、Discord 等。</p>
<p>Bot 大部分只有两种接收消息的方法：长轮询和 Webhook。</p>
<p>长轮询意味着 Bot 需要每隔一段时间（我们称之为心跳周期），向服务器请求最新消息，OpenClaw 的大部分连接器就是这么做的。</p>
<p>Webhook 则是一种更高效的方式，它允许服务器在有新消息时立即通知 Bot，而不需要 Bot 主动请求。</p>
<p>在 ClawLess 内部使用 <a href="https://chat-sdk.dev">Chat SDK</a> 来通过 WebHook 连接各个 IM。</p>
<p>Chat SDK 特别简单，只需要你创建一个 Chat 实例。</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-comment">// Next: lib/bot.ts</span>
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">Chat</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;chat&quot;</span>;

<span class="hljs-keyword">const</span> bot = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Chat</span>({
  <span class="hljs-attr">userName</span>: <span class="hljs-string">&quot;mybot&quot;</span>,
  <span class="hljs-attr">adapters</span>: { slack },
  <span class="hljs-attr">state</span>: <span class="hljs-title function_">createRedisState</span>(),
});

bot.<span class="hljs-title function_">onNewMention</span>(<span class="hljs-title function_">async</span> (thread, message) =&gt; {
  <span class="hljs-keyword">await</span> agent.<span class="hljs-title function_">handleMessage</span>(message);
});
</code></pre>
<p>然后暴露一个适用于 WebHook 的 API 端口。</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-comment">// Next: app/api/webhooks/slack/route.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> <span class="hljs-variable constant_">POST</span> = bot.<span class="hljs-property">webhooks</span>.<span class="hljs-property">slack</span>;
</code></pre>
<h2 id="最后">最后</h2>
<p>看到这里，你会发现构建 Agent 的思路其实很简单。</p>
<p>我们特别希望看到你搭配这些，构建自己的 Agent，这真的特别有意思。</p>
<p>另外，我们在构建一个通用的沙盒虚拟 Bash，它适合任何一个 JS Runtime 的环境，以及 ServerLess 和嵌入式，如果你想了解，可以看看我们的 <a href="https://niapya.github.io/ai-sdk-x/">AI-SDK-X 文档</a>.</p>
]]></content:encoded>
            <author>Niapya</author>
            <category>编程</category>
            <category>Serverless</category>
            <category>Agent</category>
            <category>Vercel</category>
        </item>
        <item>
            <title><![CDATA[Welcome to Niapya's Blog]]></title>
            <link>https://niapya.github.io/posts/welcome</link>
            <guid isPermaLink="false">https://niapya.github.io/posts/welcome</guid>
            <pubDate>Sun, 26 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[对，我终于有了一个博客。
2026 年是 Vibe Coding 大火的一年，我们习惯了使用 Code Agent 来编写任何项目，原来的「手搓」代码的方式被调侃为「古法编程」，这个时候大部分人都没有动手习惯了，如果他们希望有一个博客，直接 Vibe 一个 Next.js 模版，甚至直接用现成的 SaaS 博客平台加上 Skills 让 Agent 帮忙写文章就好了。
但是我认为，博客作为自己的平]]></description>
            <content:encoded><![CDATA[<p>对，我终于有了一个博客。</p>
<p>2026 年是 Vibe Coding 大火的一年，我们习惯了使用 Code Agent 来编写任何项目，原来的「手搓」代码的方式被调侃为「古法编程」，这个时候大部分人都没有动手习惯了，如果他们希望有一个博客，直接 Vibe 一个 <code>Next.js</code> 模版，甚至直接用现成的 <code>SaaS</code> 博客平台加上 <code>Skills</code> 让 Agent 帮忙写文章就好了。</p>
<p>但是我认为，博客作为自己的平台，理应是由作者掌控一切的，所以我希望可以从无到有做出一个自己的博客框架、自己的博客样式以及博客内容。</p>
<h2 id="制作博客框架">制作博客框架</h2>
<p>由于这类网站通常不需要服务器端逻辑，我们把博客这类网站称之为「静态网站」，所以我要做的是一个「<a href="https://developer.mozilla.org/en-US/docs/Glossary/SSG">静态站点生成器</a>」（Static Site Generator，SSG）。</p>
<p>SSG 的流程很简单：获取内容源并解析成文档，使用模板渲染，输出为网站文件，而我希望掌控这一切。</p>
<p>第一步是获取内容源并解析成文档。</p>
<p>内容源的来源可能是任何东西，数据库、CMS、Markdown 文件等等，所以我们创建了一个统一的 Interface，类似于下面。</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">definePost</span>(<span class="hljs-params">
	<span class="hljs-attr">input</span>: {
		title: <span class="hljs-built_in">string</span>;
		category?: <span class="hljs-built_in">string</span>[];
		tags?: <span class="hljs-built_in">string</span>[];
		content: <span class="hljs-built_in">string</span>;
	}
</span>): <span class="hljs-title class_">Post</span>;
</code></pre>
<p>这样一来，我们可以在任何地方以任何方式定义我们的内容了。未来我们可以使用插件，从文件路由、数据库来源，甚至浏览器中获取内容。</p>
<p>博客的内容 <code>content</code> 通常以 Markdown 格式编写，所以我使用 <code>marked</code> 来把 Markdown 编译为 HTML，并配合 <code>marked-highlight</code> 与 <code>highlight.js</code> 提供代码高亮。</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-keyword">import</span> hljs <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;highlight.js&quot;</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">Marked</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;marked&quot;</span>;
<span class="hljs-keyword">import</span> { markedHighlight } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;marked-highlight&quot;</span>;


<span class="hljs-keyword">const</span> markdownRenderer = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Marked</span>({
	<span class="hljs-attr">breaks</span>: <span class="hljs-literal">false</span>,
	<span class="hljs-attr">gfm</span>: <span class="hljs-literal">true</span>,
});

markdownRenderer.<span class="hljs-title function_">use</span>(
	<span class="hljs-title function_">markedHighlight</span>({
		<span class="hljs-attr">emptyLangClass</span>: <span class="hljs-string">&quot;hljs&quot;</span>,
		<span class="hljs-attr">langPrefix</span>: <span class="hljs-string">&quot;hljs language-&quot;</span>,
		<span class="hljs-title function_">highlight</span>(<span class="hljs-params">code, language</span>) {
			<span class="hljs-keyword">const</span> highlightLanguage = hljs.<span class="hljs-title function_">getLanguage</span>(language)
				? language
				: <span class="hljs-string">&quot;plaintext&quot;</span>;

			<span class="hljs-keyword">return</span> hljs.<span class="hljs-title function_">highlight</span>(code, { <span class="hljs-attr">language</span>: highlightLanguage }).<span class="hljs-property">value</span>;
		},
	}),
);

<span class="hljs-keyword">await</span> markdownRenderer.<span class="hljs-title function_">parse</span>(markdown, { renderer });
</code></pre>
<p>第二步是使用模板渲染。</p>
<p>由于我们要编写的是网站文件，在大多数 SSG 中，这个模板一般会和 HTML 结构强相关。如果选择自定义模板语法，就需要自己定义 AST 和编译器来处理，比如 Astro 的 <code>.astro</code> 模板本身就是一套框架自有的组件语法。与此同时，你还需要开发用于类型检查和语法高亮的工具链，这实在不利好开发者体验。</p>
<p>还有一些 SSG 使用 React/Vue 组件来编写模板，这样的模板虽然开发体验不错，但它们通常会和对应框架的生态绑定得更深。如果处理不当，甚至可能默认引入客户端 JavaScript。对于 SSG 来说，默认引入任何不必要的客户端 JavaScript 都是不明智的。</p>
<p>所以我选择了一个轻量的方案 —— 由 <code>Hono/JSX</code> 驱动的 JSX 来编写模板。它可以在使用 JSX 语法的情况下进行服务端渲染，最终输出 HTML 字符串，同时保持类型安全。对于纯静态页面来说，最终输出的 HTML 文件也不需要包含任何框架相关的客户端代码。</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="tsx"><code class="language-tsx"><span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">HtmlArticle</span>(<span class="hljs-params">{ html }</span>) {
	<span class="hljs-keyword">return</span> (
		<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>
			<span class="hljs-attr">dangerouslySetInnerHTML</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">__html:</span> <span class="hljs-attr">html</span> }}
		/&gt;</span></span>
	);
}

<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Hono</span>()
	.<span class="hljs-title function_">get</span>(<span class="hljs-string">&quot;/&quot;</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {
		<span class="hljs-keyword">return</span> c.<span class="hljs-title function_">html</span>(<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">HtmlArticle</span> <span class="hljs-attr">html</span>=<span class="hljs-string">{markdownRenderer.parse(markdown)}</span> /&gt;</span></span>)
	})
</code></pre>
<p>第三步是输出为网站文件。</p>
<p>定义好内容和模板之后，我们需要定义网站的结构（也可以称之为路由）。既然我们使用 <code>Hono/JSX</code>，那我们可以通过一个 HTTP 框架来定义路由，Hono 就是这样的。而且借助 Hono 框架，我们可以轻松启动开发服务器。</p>
<blockquote>
<p>在后续，我们还可以通过这种方式进行服务端渲染（SSR），但目前我们先专注于 SSG。</p>
</blockquote>
<p>Hono 提供了 <code>toSSG</code> 方法，能够在构建时把 Hono 应用的路由预渲染成静态文件，这样我们就可以通过 Hono 来输出静态文件了。</p>
<h2 id="设置博客样式">设置博客样式</h2>
<p>在 JSX 中设置 CSS 一直是一个难题，比如 CSS Modules，直到诸如 <code>TailwindCSS</code> 这样的原子化 CSS 方案出现。</p>
<p>我们选择了 <code>UnoCSS</code> 作为我们的 CSS 解决方案，因为它可以按需生成原子化 CSS，并且提供了可编程的生成器 API，可以直接把 CSS 类名编译成 CSS 代码。</p>
<pre class="my-4 w-full max-w-full box-border min-w-0 overflow-x-auto overflow-y-hidden border border-stone-950 bg-stone-200 p-4 font-mono shadow-sm dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100 before:mb-2 before:block before:w-fit before:border before:border-stone-950 before:bg-stone-200 before:px-2 before:py-1 before:font-mono before:text-xs before:font-bold before:text-stone-700 dark:before:border-stone-500 dark:before:bg-stone-800 dark:before:text-stone-300 before:content-[attr(data-language)]" data-language="ts"><code class="language-ts"><span class="hljs-keyword">import</span> { createGenerator, presetWind4 } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;unocss&quot;</span>;

<span class="hljs-keyword">const</span> uno = <span class="hljs-keyword">await</span> <span class="hljs-title function_">createGenerator</span>({
	<span class="hljs-attr">presets</span>: [<span class="hljs-title function_">presetWind4</span>()],
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">compileAtomicCss</span>(<span class="hljs-params"><span class="hljs-attr">input</span>: <span class="hljs-built_in">string</span></span>): <span class="hljs-title class_">Promise</span>&lt;<span class="hljs-built_in">string</span>&gt; {
	<span class="hljs-keyword">const</span> { css } = <span class="hljs-keyword">await</span> uno.<span class="hljs-title function_">generate</span>(input);
	<span class="hljs-keyword">return</span> css;
}
</code></pre>
<p>默认的博客样式我选择了一个经典的麦金塔风格，我们正在计划使用 Figma 并制作相关的 UI 组件库，以打造一个复古风格的博客主题。</p>
<h2 id="后续">后续</h2>
<p>你也许会发现，本博客没有用到任何客户端 JavaScript，这正是我想要的，对于一个静态博客，他不需要客户端 JavaScript 的引入。</p>
<p>你可以在 GitHub 上找到这个项目的源代码。</p>
<p>我们未来应该会把这个项目变成一个真正的框架，然后推出一套自己的 UI 组件库。</p>
<p>以及下面的功能：</p>
<ul>
<li>插件支持的 RSS 与 Sitemap 功能</li>
<li>基于 HTML Form 的搜索和评论系统</li>
<li>在线编译器</li>
<li>以及可能的 SSR 全栈支持</li>
</ul>
<p>因为我们没有用到 <code>Vite</code>，也尽量避免依赖只属于 <code>Node.js</code> 环境的模块，所以这个框架未来甚至可以在浏览器中直接运行。</p>
]]></content:encoded>
            <author>Niapya</author>
            <category>编程</category>
            <category>SSG</category>
            <category>Markdown</category>
            <category>Hono</category>
        </item>
    </channel>
</rss>