Spaces:
Running
Running
| # Real Agent 实施计划:从 Mockup → 真正 AI Agent | |
| ## 进行中记录(2026-06-04 01:40 CST) | |
| ### 当前本地验收清单 | |
| | 用户需求 | 当前状态 | 证据 | | |
| |------|------|------| | |
| | 合并本线程约定的 `@` / `/` / detail chat 能力 | ✅ 本地已落实 | [components/AppShell.tsx](/Users/tigersmacbookair/Documents/MySite/CoDEVX/components/AppShell.tsx), [components/ChatPanel.tsx](/Users/tigersmacbookair/Documents/MySite/CoDEVX/components/ChatPanel.tsx), [components/WorkPackageDetail.tsx](/Users/tigersmacbookair/Documents/MySite/CoDEVX/components/WorkPackageDetail.tsx) | | |
| | `@` / `/` 不再因为 invalid JSON 全面失效 | ✅ 本地已落实 | [app/api/chat/route.ts](/Users/tigersmacbookair/Documents/MySite/CoDEVX/app/api/chat/route.ts), [lib/llm-response.ts](/Users/tigersmacbookair/Documents/MySite/CoDEVX/lib/llm-response.ts), [app/api/chat/route.test.ts](/Users/tigersmacbookair/Documents/MySite/CoDEVX/app/api/chat/route.test.ts) | | |
| | 只修改当前 work package,而不是误改整板 | ✅ 本地已落实 | [lib/chat-request-context.ts](/Users/tigersmacbookair/Documents/MySite/CoDEVX/lib/chat-request-context.ts), [app/api/live-integration.test.ts](/Users/tigersmacbookair/Documents/MySite/CoDEVX/app/api/live-integration.test.ts) | | |
| | Agent Log 可完整记录并上下滚动 | ✅ 本地已落实 | [components/AgentLogPanel.tsx](/Users/tigersmacbookair/Documents/MySite/CoDEVX/components/AgentLogPanel.tsx), [components/AgentLogPanel.test.tsx](/Users/tigersmacbookair/Documents/MySite/CoDEVX/components/AgentLogPanel.test.tsx) | | |
| | detail 里可对具体句子提问 | ✅ 本地已落实 | [components/MarkdownContent.tsx](/Users/tigersmacbookair/Documents/MySite/CoDEVX/components/MarkdownContent.tsx), [components/WorkPackageDetail.tsx](/Users/tigersmacbookair/Documents/MySite/CoDEVX/components/WorkPackageDetail.tsx), [components/MarkdownContent.test.tsx](/Users/tigersmacbookair/Documents/MySite/CoDEVX/components/MarkdownContent.test.tsx) | | |
| | model settings 可明确测试连接 | ✅ 本地已落实 | [components/ChatPanel.tsx](/Users/tigersmacbookair/Documents/MySite/CoDEVX/components/ChatPanel.tsx), [components/ChatPanel.test.tsx](/Users/tigersmacbookair/Documents/MySite/CoDEVX/components/ChatPanel.test.tsx), [app/api/test-connection/route.ts](/Users/tigersmacbookair/Documents/MySite/CoDEVX/app/api/test-connection/route.ts) | | |
| | chat 不再把 JSON 原样吐给用户 | ✅ 本地已落实 | [lib/llm-response.ts](/Users/tigersmacbookair/Documents/MySite/CoDEVX/lib/llm-response.ts), [lib/llm-response.test.ts](/Users/tigersmacbookair/Documents/MySite/CoDEVX/lib/llm-response.test.ts) | | |
| | 做完整集成测试,不只靠局部手测 | ✅ 本地已落实 | [app/api/live-integration.test.ts](/Users/tigersmacbookair/Documents/MySite/CoDEVX/app/api/live-integration.test.ts) | | |
| ### 本轮正在推进 | |
| - 收紧 work package detail 里的局部聊天体验 | |
| - 让 `@` / `/` 指令稳定落到当前选中的 package 上 | |
| - 让 latest output 里的具体段落和列表项可以直接发起 scoped ask | |
| - 保持 Agent Log 为完整、可滚动、按时间顺序的纯文本进度区 | |
| ### 本轮已落实 | |
| 1. detail 视图里的普通问题会自动按当前 package 的 `ask` 处理,不再误触发整板自动化。 | |
| 2. 直接发送 `/ask`、`/plan`、`/change`、`/execute` 时,会命中当前 package,而不是依赖先点自动补全。 | |
| 3. LLM 返回非 JSON 时,`ask` 会尽量保留文本答复,`plan` / `change` 会走本地 structured fallback,不再只报 invalid JSON。 | |
| 4. latest output 区域现在支持更细粒度提问: | |
| - `Ask about latest output` | |
| - `Ask about this paragraph` | |
| - `Ask about this item` | |
| 5. Agent Log 会持续累积关键动作,按时间顺序展示,并自动滚到最新记录。 | |
| ### 本轮验证结果 | |
| - `npm test`:18 个测试文件,44 个测试通过 | |
| - `npm run lint`:通过 | |
| - `npm run build`:通过 | |
| - 路由级 fake-live 集成验证通过: | |
| - `/api/test-connection` 可返回 `Connected to the configured model.` | |
| - `@SRS ask ...` 返回人类可读说明,不改 board | |
| - `@SRS plan ...` 只更新 `wp-srs` | |
| - `@SRS change ...` 只更新 `wp-srs` | |
| - `/plan ...` 通过选中 package 的客户端路由链路后,仍只更新 `wp-srs` | |
| ## 一、当前状态 | |
| | 项目 | 状态 | | |
| |------|------| | |
| | UI 两栏布局 + 12 个种子包 | ✅ 已完成 | | |
| | 命令解析器(@包名 模式 指令) | ✅ 已完成,新旧两版并存 | | |
| | 编辑器自动补全(@package / /mode) | ✅ 已完成 | | |
| | Mock Agent(硬编码返回) | ✅ 已完成,但依赖旧版 parser | | |
| | 系统提示词合约 | ✅ 已完成(见 prompts/work-package-system-prompt.md) | | |
| | LLM API 路由 | ❌ 不存在 | | |
| | 根目录 package.json | ❌ 不存在 | | |
| | 真实 LLM 集成 | ❌ 不存在 | | |
| | 加载/错误/连接状态 | ❌ 不存在 | | |
| ## 二、目标架构 | |
| ``` | |
| 用户输入 "SRS plan 拆成组件模块并定义接口" | |
| │ | |
| ▼ | |
| parseCommand() ← 解析出 { mode: "plan", matchedWorkPackageId, instruction } | |
| │ | |
| ▼ | |
| Agent.orchestrate() ← 统一入口 | |
| │ | |
| ├── LLM_API_KEY 存在? | |
| │ │ | |
| │ YES → POST /api/chat ──→ DeepSeek API | |
| │ │ │ | |
| │ │ 返回 { assistantMessage, boardAction } | |
| │ │ │ | |
| │ │ 前端执行 boardAction | |
| │ │ (新增 task / 修改字段 / 追加 output) | |
| │ │ | |
| │ NO → Mock Agent 回退 | |
| │ | |
| ▼ | |
| 更新 WorkPackage[] + ChatMessage[] | |
| │ | |
| ▼ | |
| UI 重新渲染 | |
| ``` | |
| 关键变化:**Agent 不再自己 mutation WorkPackage,而是返回一个 `boardAction`**。由前端统一执行这个 action。Mock Agent 和 LLM Agent 共用同一个接口签名。 | |
| ## 三、实施步骤(共 8 步,约 4-6 小时) | |
| --- | |
| ### 第 0 步:修复项目结构,使代码可构建(前置条件,30 分钟) | |
| **问题**:当前代码无法构建。根目录缺少 package.json,新旧 parser 并存,mock-agent 未迁移。 | |
| **操作**: | |
| 1. 在根目录创建 `package.json`,包含所有依赖 | |
| 2. 将 `data/work-packages.seed.json` 复制到根目录 `data/` | |
| 3. 创建根目录 `lib/mock-agent.ts`,适配新 `parseCommand()` API | |
| 4. 迁移 `app/` 和 `components/` 到根目录 | |
| 5. 确保 `npm run dev` 可正常启动 | |
| --- | |
| ### 第 1 步:统一 Agent 接口(30 分钟) | |
| 当前 mock-agent 直接返回变更后的 WorkPackage[]。真实 LLM Agent 应该返回一个**变更指令**,由调用方执行。 | |
| ```typescript | |
| // lib/agent-types.ts —— 新增文件 | |
| export type BoardActionType = "none" | "update_task" | "add_tasks" | |
| | "append_output" | "update_section" | "change_status"; | |
| export type BoardAction = { | |
| type: BoardActionType; | |
| workPackageId: string; | |
| payload: Record<string, unknown>; | |
| }; | |
| export type AgentTurnResult = { | |
| assistantMessage: string; | |
| boardActions: BoardAction[]; | |
| }; | |
| export type AgentContext = { | |
| messages: ChatMessage[]; | |
| workPackages: WorkPackage[]; | |
| parsedCommand: ParsedCommand & { matchedWorkPackageId?: string }; | |
| }; | |
| ``` | |
| **为什么这样设计**: | |
| - Mock Agent 和 LLM Agent 返回同一种结构,切换时无需改 UI 代码 | |
| - `boardActions` 是个数组——允许一次对话执行多个操作(例如:plan + change 同时发生) | |
| - 把"生成指令"和"执行指令"拆开,便于测试 | |
| --- | |
| ### 第 2 步:创建 DeepSeek API 路由(45 分钟) | |
| **文件**:`app/api/chat/route.ts` | |
| **路由逻辑**: | |
| ``` | |
| POST /api/chat | |
| Body: { messages: ChatMessage[], workPackages: WorkPackage[], | |
| parsedCommand: ParsedCommand } | |
| 1. 从 process.env 读取 LLM_API_KEY, LLM_API_BASE_URL, LLM_MODEL | |
| 2. 构造 messages 数组: | |
| - system: work-package-system-prompt.md 内容 | |
| - system: 当前所有 workPackages 的 JSON(供 LLM 参考) | |
| - user: 当前用户消息 | |
| 3. 调用 DeepSeek API(POST {baseUrl}/chat/completions) | |
| 4. 解析响应中的 JSON(要求 response_format: { type: "json_object" }) | |
| 5. 返回 { assistantMessage, boardActions } | |
| ``` | |
| **环境变量**(`.env.local`): | |
| ```env | |
| LLM_API_BASE_URL=https://api.deepseek.com | |
| LLM_API_KEY=sk-your-deepseek-key | |
| LLM_MODEL=deepseek-chat | |
| ``` | |
| **错误处理**: | |
| - API key 缺失 → 返回 503,附带 `fallback: true` 标记 | |
| - DeepSeek 返回非 JSON → 返回 502,附带原始文本 | |
| - 超时(15 秒)→ 返回 504,前端回退到 mock | |
| --- | |
| ### 第 3 步:实现 BoardAction 执行器(30 分钟) | |
| **文件**:`lib/board-reducer.ts` | |
| 这是一个纯函数:接收 `WorkPackage[]` + `BoardAction[]`,返回新的 `WorkPackage[]`。 | |
| ```typescript | |
| export function applyBoardActions( | |
| workPackages: WorkPackage[], | |
| actions: BoardAction[] | |
| ): WorkPackage[] { | |
| return workPackages.map((wp) => { | |
| const action = actions.find((a) => a.workPackageId === wp.id); | |
| if (!action) return wp; | |
| switch (action.type) { | |
| case "add_tasks": | |
| return { ...wp, tasks: [...wp.tasks, ...(action.payload.tasks as WorkPackageTask[])] }; | |
| case "append_output": | |
| return { ...wp, outputs: [action.payload.output as WorkPackageOutput, ...wp.outputs] }; | |
| case "update_section": | |
| return { ...wp, coreSections: [...wp.coreSections, action.payload.section as string] }; | |
| case "change_status": | |
| return { ...wp, status: action.payload.status as WorkPackageStatus }; | |
| case "update_task": | |
| return { ...wp, tasks: wp.tasks.map((t) => | |
| t.id === action.payload.taskId ? { ...t, ...action.payload.patch } : t | |
| )}; | |
| default: | |
| return wp; | |
| } | |
| }); | |
| } | |
| ``` | |
| **为什么需要这个**: | |
| - Mock Agent 当前直接在内部 mutation WorkPackage——这污染了 mock agent 的逻辑 | |
| - 拆出执行器后,mock agent 只负责生成 action,不碰数据 | |
| - LLM 返回的 `boardAction` JSON 也走同一个执行器——一条路径,两种来源 | |
| --- | |
| ### 第 4 步:重构 Mock Agent 使其返回 BoardAction(20 分钟) | |
| 当前 mock-agent 直接返回突变后的 WorkPackage[]。改为返回 `AgentTurnResult`: | |
| ```typescript | |
| // 旧接口 | |
| function runMockAgentTurn(input, workPackages): { assistantMessage, workPackages } | |
| // 新接口 | |
| function runMockAgentTurn(context: AgentContext): AgentTurnResult | |
| ``` | |
| Mock Agent 内部不再直接修改 WorkPackage,而是构建 `BoardAction[]`。例如: | |
| ``` | |
| mode === "plan" → | |
| boardActions = [ | |
| { type: "add_tasks", workPackageId: "wp-srs", payload: { tasks: [...] } }, | |
| { type: "change_status", workPackageId: "wp-srs", payload: { status: "in_progress" } } | |
| ] | |
| ``` | |
| --- | |
| ### 第 5 步:创建 Agent 编排器(30 分钟) | |
| **文件**:`lib/agent.ts` | |
| ```typescript | |
| export async function runAgentTurn(context: AgentContext): Promise<AgentTurnResult> { | |
| // 1. 检查是否有 API 配置 | |
| const hasLLMConfig = !!process.env.NEXT_PUBLIC_LLM_ENABLED; | |
| if (!hasLLMConfig) { | |
| // Mock 回退 | |
| return runMockAgentTurn(context); | |
| } | |
| // 2. 调用 LLM API | |
| try { | |
| const response = await fetch("/api/chat", { | |
| method: "POST", | |
| body: JSON.stringify(context), | |
| }); | |
| if (!response.ok) { | |
| // API 失败,回退到 mock | |
| return runMockAgentTurn(context); | |
| } | |
| return await response.json(); | |
| } catch { | |
| // 网络错误,回退到 mock | |
| return runMockAgentTurn(context); | |
| } | |
| } | |
| ``` | |
| **兜底策略**:三层保护 | |
| 1. 无 API key → mock | |
| 2. API 返回错误 → mock | |
| 3. 网络错误 → mock | |
| 任何时候 UI 都不会崩溃,只是功能降级。 | |
| --- | |
| ### 第 6 步:在 UI 层接入真实 Agent(30 分钟) | |
| 修改 `AgenticPmWorkbench.tsx` 中的 `handleSend()`: | |
| ```typescript | |
| // 旧代码 | |
| function handleSend() { | |
| const result = runMockAgentTurn(content, workPackages); | |
| setMessages(...) | |
| setWorkPackages(result.workPackages) | |
| } | |
| // 新代码 | |
| async function handleSend() { | |
| setIsLoading(true); | |
| const context: AgentContext = { | |
| messages, | |
| workPackages, | |
| parsedCommand: parseCommand(content, workPackages), | |
| }; | |
| const result = await runAgentTurn(context); | |
| // 统一的 board action 执行 | |
| const nextPackages = applyBoardActions(workPackages, result.boardActions); | |
| setMessages(...) | |
| setWorkPackages(nextPackages) | |
| setIsLoading(false); | |
| } | |
| ``` | |
| **注意**:新 UI 需要的功能: | |
| - `isLoading` 状态 —— 在 LLM 调用期间显示旋转动画或 "Agent is thinking..." | |
| - `ConnectionStatus` 指示器 —— 类型已定义(idle/checking/connected/error),需接入 UI | |
| - 错误提示 —— API 失败时 toast 通知 | |
| --- | |
| ### 第 7 步:UX 打磨(45 分钟) | |
| 1. **连接状态指示器** | |
| - 顶部栏显示一个圆点:绿色 = connected,灰色 = mock mode,红色 = error | |
| - 实现 `ConnectionStatus` 类型的状态机 | |
| 2. **加载动画** | |
| - 等待 LLM 回复时,聊天区显示波浪动画 | |
| - 禁用发送按钮防止重复提交 | |
| 3. **Mock Mode 标识** | |
| - 未配置 API key 时,在标题栏旁显示 "MOCK MODE" 标签 | |
| 4. **错误 Toast** | |
| - API 调用失败时弹出短暂提示,自动消失 | |
| - 显示回退到 mock 的提示 | |
| --- | |
| ## 四、调用链最终形态 | |
| ``` | |
| 用户输入 "SRS plan 拆成组件模块并定义接口" | |
| │ | |
| ▼ | |
| parseCommand(text, packages) ← 纯同步 | |
| │ | |
| ▼ | |
| AgentContext { messages, workPackages, parsedCommand } | |
| │ | |
| ▼ | |
| runAgentTurn(context) ← 异步 | |
| │ | |
| ├── LLM 可用? | |
| │ │ | |
| │ YES──→ fetch("/api/chat", { body: context }) | |
| │ │ │ | |
| │ │ ▼ | |
| │ │ DeepSeek API | |
| │ │ POST /chat/completions | |
| │ │ model: deepseek-chat | |
| │ │ │ | |
| │ │ ▼ | |
| │ │ JSON: { assistantMessage, boardActions } | |
| │ │ | |
| │ NO / ERROR ──→ runMockAgentTurn(context) | |
| │ │ | |
| │ ▼ | |
| │ AgentTurnResult { assistantMessage, boardActions } | |
| │ | |
| ▼ | |
| AgentTurnResult | |
| │ | |
| ▼ | |
| applyBoardActions(workPackages, boardActions) ← 纯函数 | |
| │ | |
| ▼ | |
| setState({ messages: [...], workPackages: [...] }) | |
| │ | |
| ▼ | |
| UI 重新渲染 | |
| ``` | |
| ## 五、实施优先级和预估 | |
| | 步骤 | 内容 | 预估 | 优先级 | | |
| |------|------|------|--------| | |
| | 0 | 修复项目结构,使可构建 | 30 分钟 | 🔴 阻塞 | | |
| | 1 | 统一 Agent 接口(AgentContext, AgentTurnResult, BoardAction) | 30 分钟 | 🔴 基础 | | |
| | 2 | 创建 `/api/chat` 路由(DeepSeek) | 45 分钟 | 🔴 核心 | | |
| | 3 | 实现 `board-reducer.ts`(执行 BoardAction) | 30 分钟 | 🟡 基础 | | |
| | 4 | 重构 Mock Agent 返回 BoardAction | 20 分钟 | 🟡 基础 | | |
| | 5 | 创建 Agent 编排器(LLM + Mock 回退) | 30 分钟 | 🔴 核心 | | |
| | 6 | UI 接入真实 Agent | 30 分钟 | 🟡 关键 | | |
| | 7 | UX 打磨(加载态/状态指示/mock标识) | 45 分钟 | 🟢 重要 | | |
| **总预估**:约 4-5 小时(含测试和调试) | |
| ## 六、关键设计决策 | |
| ### 为什么不使用流式输出(SSE)? | |
| MVP 阶段不实现流式。原因: | |
| - 结构化 JSON 响应不适合流式输出(必须等完整 JSON 才能 parse) | |
| - 简化错误处理和重试逻辑 | |
| - 后续可升级,在 `/api/chat` 路由添加 `stream: true` 参数 | |
| ### 为什么 BoardAction 走前端执行而不是 API 路由直接执行? | |
| - **透明性**:用户可以在 agent log 中看到每个 action 的执行结果 | |
| - **可撤销**:未来可以支持 undo(因为 state 变化是可追溯的) | |
| - **一致性**:Mock Agent 和 LLM Agent 走同一条执行路径 | |
| - **安全性**:API 路由不需要访问 React state | |
| ### 为什么 system prompt 里保留模拟免责声明? | |
| 真 Agent 也可以生成模拟输出。MVP 阶段即使接了 DeepSeek,所有 execute 仍然是模拟的——不会真正去跑测试、查专利、生成图像。免责声明是产品特性而非 mock 专用的。 | |
| ## 七、DeepSeek 接入要点 | |
| - **Base URL**: `https://api.deepseek.com` | |
| - **Model**: `deepseek-chat`(性价比最优,支持 JSON mode) | |
| - **特殊配置**: 无需特殊配置,标准 OpenAI SDK 直接兼容 | |
| - **JSON mode**: DeepSeek 支持 `response_format: { type: "json_object" }` | |
| - **Token 限制**: 上下文 64K tokens,足够塞 12 个 WorkPackage + 对话历史 | |
| - **提示**: system prompt 里写清楚 JSON 格式要求,user message 里加上 "Respond with JSON only." | |
| --- | |
| > **下一步**:确认此计划后,从第 0 步开始执行。 | |