构建我的 AI 代理:技术实现详解
从零开始,用 Vercel AI SDK + Astro + Cloudflare Workers 构建一个能回答我简历问题的 AI 代理。
你可能注意到了网站右下角的聊天按钮。点开它,你可以和”我”对话——准确地说,是和一个了解我简历的 AI 代理对话。
这篇文章将详细介绍这个 AI 代理的技术实现。
为什么要做这个?
传统简历是静态的。访客只能被动阅读,无法主动提问。而 AI 代理可以:
- 交互式体验:访客可以直接问”你会什么技术?”
- 个性化回答:根据提问角度组织信息
- 技术展示:本身就证明了我的 AI 工程能力
技术栈选择
| 组件 | 技术 | 理由 |
|---|---|---|
| 框架 | Astro | SSR 支持 + 静态优先 |
| 前端 | React + Vercel AI SDK | 流式渲染 + 类型安全 |
| 后端 | Astro API Route | 与前端同项目,部署简单 |
| 部署 | Cloudflare Workers | 边缘计算,全球低延迟 |
| 模型 | NVIDIA NIM API | 兼容 OpenAI 接口 |
系统架构
整个流程分三步:
- ChatDialog (React) → 用户输入消息,调用
useChat发送请求 - /api/chat (Astro API) → 接收请求,调用
streamText生成流式响应 - NVIDIA NIM (LLM) → 返回 AI 回复,逐字流式传输回前端
后端实现:流式响应
核心代码在 src/pages/api/chat.ts:
import { createOpenAI } from '@ai-sdk/openai';
import { streamText, convertToModelMessages } from 'ai';
import { OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL } from 'astro:env/server';
const SYSTEM_PROMPT = `你是 Leon Liu(刘良权)的 AI 代理...`;
export const POST: APIRoute = async ({ request }) => {
const { messages } = await request.json();
const openai = createOpenAI({
baseURL: OPENAI_BASE_URL,
apiKey: OPENAI_API_KEY,
});
const result = streamText({
model: openai.chat(OPENAI_MODEL),
system: SYSTEM_PROMPT,
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse({
sendReasoning: true,
});
};
关键设计决策
- 使用
astro:env:类型安全的环境变量,本地.env和 Cloudflare Secrets 自动兼容 - 流式响应:
streamText+toUIMessageStreamResponse实现打字机效果 - System Prompt 即知识库:把完整简历嵌入 Prompt,无需 RAG
前端实现:实时渲染
src/components/Chatbot/ChatDialog.tsx 使用 Vercel AI SDK 的 useChat Hook:
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({
api: '/api/chat',
}),
});
Markdown 渲染
AI 回复支持完整 Markdown,包括代码高亮:
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import CodeBlock from './CodeBlock';
<Markdown
remarkPlugins={[remarkGfm]}
components={{
code({ className, children }) {
const match = /language-(\w+)/.exec(className || '');
return match ? (
<CodeBlock language={match[1]}>{children}</CodeBlock>
) : (
<code>{children}</code>
);
},
}}
>
{response}
</Markdown>
思考过程展示
部分模型(如 DeepSeek)会输出 <think> 标签包裹的思考过程。我做了特殊处理:
const parseThinkContent = (text: string) => {
const thinkMatch = text.match(/<think>([\s\S]*?)<\/think>/);
if (thinkMatch) {
return {
thinking: thinkMatch[1].trim(),
response: text.replace(/<think>[\s\S]*?<\/think>/, '').trim(),
};
}
return { thinking: '', response: text };
};
思考内容会显示在 <details> 折叠块中,让用户可以查看 AI 的推理过程。
Neobrutalism 样式
聊天界面遵循网站的 Neobrutalism 设计语言:
.chat-dialog {
border: 3px solid #000;
box-shadow: 8px 8px 0 #000;
border-radius: 16px;
}
.chat-message-content {
border: 3px solid #000;
box-shadow: 4px 4px 0 #000;
border-radius: 8px;
}
粗边框 + 硬阴影 + 鲜艳色彩 = Neobrutalism。
部署到 Cloudflare
使用 @astrojs/cloudflare adapter:
// astro.config.mjs
export default defineConfig({
output: 'server',
adapter: cloudflare({
imageService: 'compile',
}),
});
环境变量在 Cloudflare Dashboard 配置为 Secrets,不写入代码仓库。
经验总结
- Vercel AI SDK 很好用:统一了前后端接口,流式渲染开箱即用
- System Prompt 即 RAG:对于简历这种小规模知识,直接嵌入 Prompt 比建向量库更简单
- 边缘部署很重要:Cloudflare Workers 让全球访客都有低延迟体验
- 设计即展示:聊天界面本身就是作品展示的一部分
下一步
- 添加对话历史持久化(Cloudflare KV)
- 集成更多数据源(GitHub 项目、博客文章)
- 多语言支持
有问题?直接点右下角问”我”吧 😉