CoDEVX / real-agent-plan.md
CodexMacTiger
feat: live package-scoped chat and thinking logs
837e3ac
# 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 步开始执行。