过去两年,AI Agent 的讨论大多集中在后端:LangChain、AutoGPT、Function Calling……但当你真正做一个面向用户的 Agent 产品时,前端才是用户感知 Agent「在思考、在行动」的地方。
用户并不关心你的 Planner 用了哪个模型,他们关心的是:
Home Agent 就是一个专门回答这些问题的 学习型 Next.js 项目。它不追求生产级 Agent 框架的完备性,而是把 Agent 循环设计、SSE 流式协议、编排 UI 状态机 三件事拆得足够清晰,方便阅读和二次扩展。
| 维度 | 说明 |
|---|---|
| 定位 | AI Agent 前端编排学习项目 |
| 核心页面 | /agents(/ 自动跳转) |
| 核心 API | POST /api/agent(SSE trace 流) |
| 内置工具 | search_notes · calculate · current_time |
| 技术栈 | Next.js 16 · React 19 · TypeScript · Tailwind CSS 4 · Prisma · PostgreSQL |
| 工具 | 说明 |
|---|---|
search_notes | 检索知识库笔记(PostgreSQL + pg_trgm 模糊搜索,无 DB 时内存回退) |
calculate | 安全数学表达式求值 |
current_time | 返回服务器本地时间 |
flowchart LRUI["/agents 页面"] --> API["POST /api/agent"]API --> Loop["runAgentLoop"]Loop --> Plan["planAgentStep"]Plan -->|tool| Tools["executeAgentTool"]Tools --> LoopPlan -->|answer| SSE["SSE events"]SSE --> UI
一句话总结数据流:用户输入 → Agent 循环规划 → 按需调用工具 → 每步产出 trace 事件 → SSE 推流 → 前端 Hook 解析 → 编排 UI 实时更新。
.nvmrc)pnpm installcp .env.example .envdocker compose up -d dbpnpm db:setuppnpm dev
浏览器访问 http://localhost:3000/agents。
ollama pull llama3.2ollama serve
.env 默认已配置 Ollama 的 OpenAI 兼容接口。若未配置 LLM 或设置 LLM_DISABLED=1,系统会自动回退到规则规划器——这在 CI 和离线学习场景下非常实用。
Agent 的心脏在 src/lib/agent/run-loop.ts。它实现了一个 async generator,每推进一步就 yield 一个 trace 事件:
export async function* runAgentLoop(message: string,options: { signal?: AbortSignal } = {},): AsyncGenerator<AgentTraceEvent> {// ...while (steps < maxSteps) {const { plan, mock } = await planAgentStep(message, prior);yield { type: "plan", plan };if (plan.action === "answer") {yield { type: "answer", text: plan.answer, mock };yield { type: "done", steps, toolCalls, totalMs };return;}yield { type: "tool_call", tool: plan.tool, args: plan.args };const result = await executeAgentTool(plan.tool, plan.args);yield { type: "tool_result", tool: plan.tool, output: result.output };}}
AGENT_MAX_STEPS 控制(默认 4,上限 12),防止无限循环。step_metric 事件,记录 planMs、toolMs、totalMs。这种 generator + yield 事件 的模式,比「先跑完再返回 JSON」更适合流式 UI——后端每推进一步,前端就能更新一次。
src/lib/agent/planner.ts 负责「这一步该调工具还是直接回答」。
当 Ollama 或 OpenAI 兼容 API 可用时,规划器要求模型输出结构化 JSON:
const response = await client.chat.completions.create({model,temperature: 0.1,response_format: { type: "json_object" },messages: [{ role: "system", content: getPlannerSystem() },{ role: "user", content: JSON.stringify(userPayload) },],});
System Prompt 明确约束输出格式——要么 { action: "tool", tool, args, reasoning },要么 { action: "answer", answer, reasoning }。LLM 返回的 JSON 还会经过 Zod 校验(planner-schema.ts),无效则回退。
src/lib/agent/planner-mock.ts 用关键词匹配实现零依赖规划:
search_notescalculatecurrent_timeif (wantsSearch && !hasTool("search_notes")) {return {action: "tool",tool: "search_notes",args: { query: "..." },reasoning: "问题涉及知识库,先检索笔记",};}
为什么需要双轨?
POST /api/agent 返回 Content-Type: text/event-stream。每个事件块格式:
event: plandata: {"type":"plan","plan":{"action":"tool","tool":"search_notes",...}}
| type | 说明 |
|---|---|
trace | 循环阶段日志(start / plan / limit) |
plan | 规划器输出 |
tool_call | 即将执行的工具及参数 |
tool_result | 工具返回文本 |
step_metric | 单步耗时(planMs / toolMs / totalMs) |
answer | 最终回答(mock: true 表示规则回退) |
done | 循环结束统计 |
error | 错误信息 |
src/app/api/agent/route.ts 把 async generator 桥接到 Web Streams API:
const stream = new ReadableStream({async start(controller) {for await (const trace of runAgentLoop(body.message, {signal: request.signal,})) {controller.enqueue(encoder.encode(encodeSseEvent(trace.type, trace)));}controller.close();},});return new Response(stream, {headers: {"Content-Type": "text/event-stream; charset=utf-8","Cache-Control": "no-cache, no-transform",Connection: "keep-alive",},});
SSE 编码/解码被抽成独立模块 src/lib/sse.ts,前后端共用同一套解析逻辑,避免协议漂移。
无需打开浏览器,直接用 curl 观察事件流:
curl -N -X POST http://localhost:3000/api/agent -H "Content-Type: application/json" -d '{"message":"计算 1+2"}'
src/hooks/use-agent-sse.ts 是前端消费 SSE 的核心。它用 fetch + ReadableStream 按 nn 分块解析,维护以下状态:
| 状态 | 用途 |
|---|---|
lines | Trace 面板的事件行 |
phase | 工作流阶段(idle / planning / tool / answering / done / error) |
finalAnswer | 最终回答文本 |
stepMetrics | 每步耗时数据 |
stats | 总结(步数、工具调用次数、总耗时) |
isMock | 是否使用了规则规划器 |
const { run, stop, running, lines, finalAnswer, stats, stepMetrics } =useAgentStream({onEvent: (event) => console.log(event),});await run("现在几点?");
Hook 内部把原始 SSE 事件映射为人类可读的 Trace 行,例如:
plan → 调用 search_notes · 问题涉及知识库,先检索笔记tool_call → → calculate({"expression":"1+2"})tool_result → 工具输出(检索结果还会标注命中条数)src/components/agent-orchestrator.tsx 把 Hook 状态组装成完整的编排界面:
| 组件 | 职责 |
|---|---|
AgentWorkflowBar | 工作流进度条(规划 → 工具 → 回答) |
AgentTracePanel | 逐步展开的 trace 日志 |
AgentStepMetrics | 每步耗时可视化 |
AgentFinalAnswer | 最终回答展示(含 mock 标记) |
IntelligenceLearningPanel | 进阶:编排偏好与学习面板 |
页面内置快捷按钮(agent-quick-prompts.ts)和工具目录展示(tool-catalog.ts),降低首次体验门槛。用户点一下「计算 100 * 0.15 + 20」或「现在几点?」,就能立刻看到 Agent 循环的完整 trace。
每个 Agent 工具需要保持四处一致(详见 docs/add-a-tool.md):
types.ts — 工具名类型tool-catalog.ts — UI 展示文档tools.ts — 执行逻辑planner.ts + planner-schema.ts — 告诉规划器工具存在以 search_notes 为例,底层调用 src/lib/note-search.ts:
pg_trgm 扩展做模糊搜索calculate 工具则通过字符白名单 + new Function 做受限求值——学习项目可以这么写,生产环境需要沙箱方案。
src/lib/front-intelligence-preferences.ts 演示如何把用户偏好注入 prompt,并通过 localStorage 持久化:
这是可选进阶模块,不影响 Agent 核心循环。但它展示了真实产品中常见的模式——前端偏好 → prompt 增强 → Agent 行为微调。
项目使用 Prisma 管理数据模型,Docker Compose 一键启动 PostgreSQL 16:
services:db:image: postgres:16-alpineenvironment:POSTGRES_DB: home_agentPOSTGRES_USER: postgresPOSTGRES_PASSWORD: postgresports:- "5432:5432"
pnpm db:setup 会执行 prisma db push + prisma db seed,写入示例笔记供 search_notes 检索。
健康检查端点 GET /api/health 会报告 DB 连接、LLM 配置、pg_trgm 扩展是否可用——前端 Header 的 Badge 就是消费这个接口。
Home Agent 的测试策略值得借鉴:
| 层级 | 工具 | 说明 |
|---|---|---|
| 单元测试 | Vitest | 规划器 schema、SSE 解析、工具逻辑、run-loop |
| API 冒烟 | pnpm smoke | 对运行中的 dev server 发 SSE 请求 |
| E2E | Playwright | 完整页面交互流程 |
CI 中设置 LLM_DISABLED=1,确保流水线不依赖外部 LLM 服务。规则规划器让「Agent 循环 → SSE → 前端 UI」这条链路在每次 PR 中都被验证。
假设要添加一个 echo 工具(回显用户输入),按以下清单操作:
types.ts — 加入 "echo" 工具名tool-catalog.ts — 注册 UI 文档tools.ts — 实现 case "echo"planner-schema.ts — Zod 枚举加入 "echo"planner.ts — system prompt 描述新工具planner-mock.ts — 关键词规则agent-quick-prompts.ts — 快捷按钮验证:
pnpm typecheck && pnpm testpnpm dev # 另一终端pnpm smoke
在 /agents 输入「请 echo 你好」,观察 trace 是否出现 tool_call → echo → tool_result。
Home Agent 刻意保持简单,以下限制请在生产使用前知晓:
| 项目 | 现状 | 生产建议 |
|---|---|---|
| API 鉴权 | /api/agent 无鉴权 | 加 API Key / Session / Rate Limit |
| calculate 工具 | new Function 受限求值 | 独立沙箱(VM / WASM) |
| LLM 输出 | Zod 校验 + 回退 | 加重试、超时、内容过滤 |
| SSE 连接 | 无心跳 | 加 keep-alive / reconnect 策略 |
如果你想深入源码,建议按此顺序阅读:
src/lib/agent/types.ts — 事件与规划类型定义src/lib/agent/run-loop.ts — Agent 主循环src/lib/agent/planner.ts + planner-mock.ts — LLM / 规则双轨规划src/app/api/agent/route.ts — SSE 出口src/hooks/use-agent-sse.ts — 前端消费 Hooksrc/components/agent-orchestrator.tsx — 编排 UI 组装配套文档:
Home Agent 用约 80 个源文件,演示了 AI Agent 前端编排的核心链路:
几个值得带走的实践:
如果你正在学习 Agent 或准备做一个带编排 UI 的 AI 产品,不妨 clone 这个项目,从添加一个新 Tool 开始动手。
git clone https://github.com/jiaxiantao/home-agent.gitcd home-agentpnpm install && cp .env.example .envdocker compose up -d db && pnpm db:setup && pnpm dev
| 项目 | 链接 |
|---|---|
| GitHub 仓库 | github.com/jiaxiantao/… |
| Issue / 讨论 | github.com/jiaxiantao/… |
| CI 状态 | github.com/jiaxiantao/… |
git clone https://github.com/jiaxiantao/home-agent.git
EventSource API 说明Response 流式返回search_notes 模糊检索依赖的扩展文档| 书名 | 作者 | 与本项目的关联 |
|---|---|---|
| AI Engineering | Chip Huyen | LLM 应用架构、RAG、Agent 编排与工程化落地 |
| Building LLM Apps | Valentina Alto | 从零构建 LLM 应用,涵盖 Tool Calling 与对话流程设计 |
| Designing Data-Intensive Applications | Martin Kleppmann | 流式传输、事件驱动、数据一致性——理解 SSE 推送的底层视角 |
| Designing Machine Learning Systems | Chip Huyen | ML 系统工程思维,对 Agent 监控、回退策略有启发 |
| JavaScript: The Definitive Guide | David Flanagan | Async Generator、ReadableStream 等 JavaScript 异步机制参考 |