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, components/ChatPanel.tsx, components/WorkPackageDetail.tsx
@ / / 不再因为 invalid JSON 全面失效 ✅ 本地已落实 app/api/chat/route.ts, lib/llm-response.ts, app/api/chat/route.test.ts
只修改当前 work package,而不是误改整板 ✅ 本地已落实 lib/chat-request-context.ts, app/api/live-integration.test.ts
Agent Log 可完整记录并上下滚动 ✅ 本地已落实 components/AgentLogPanel.tsx, components/AgentLogPanel.test.tsx
detail 里可对具体句子提问 ✅ 本地已落实 components/MarkdownContent.tsx, components/WorkPackageDetail.tsx, components/MarkdownContent.test.tsx
model settings 可明确测试连接 ✅ 本地已落实 components/ChatPanel.tsx, components/ChatPanel.test.tsx, app/api/test-connection/route.ts
chat 不再把 JSON 原样吐给用户 ✅ 本地已落实 lib/llm-response.ts, lib/llm-response.test.ts
做完整集成测试,不只靠局部手测 ✅ 本地已落实 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 应该返回一个变更指令,由调用方执行。

// 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):

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[]

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

// 旧接口
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

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()

// 旧代码
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 步开始执行。