Spaces:
Running
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 为完整、可滚动、按时间顺序的纯文本进度区
本轮已落实
- detail 视图里的普通问题会自动按当前 package 的
ask处理,不再误触发整板自动化。 - 直接发送
/ask、/plan、/change、/execute时,会命中当前 package,而不是依赖先点自动补全。 - LLM 返回非 JSON 时,
ask会尽量保留文本答复,plan/change会走本地 structured fallback,不再只报 invalid JSON。 - latest output 区域现在支持更细粒度提问:
Ask about latest outputAsk about this paragraphAsk about this item
- 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 未迁移。
操作:
- 在根目录创建
package.json,包含所有依赖 - 将
data/work-packages.seed.json复制到根目录data/ - 创建根目录
lib/mock-agent.ts,适配新parseCommand()API - 迁移
app/和components/到根目录 - 确保
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 返回的
boardActionJSON 也走同一个执行器——一条路径,两种来源
第 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);
}
}
兜底策略:三层保护
- 无 API key → mock
- API 返回错误 → mock
- 网络错误 → 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 分钟)
连接状态指示器
- 顶部栏显示一个圆点:绿色 = connected,灰色 = mock mode,红色 = error
- 实现
ConnectionStatus类型的状态机
加载动画
- 等待 LLM 回复时,聊天区显示波浪动画
- 禁用发送按钮防止重复提交
Mock Mode 标识
- 未配置 API key 时,在标题栏旁显示 "MOCK MODE" 标签
错误 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 步开始执行。