Parcourir la source

Merge branch 'main' of https://git.yishihui.com/howard/Agent

max_liu il y a 3 semaines
Parent
commit
3e34defb3d

+ 22 - 0
agent/core/runner.py

@@ -531,6 +531,25 @@ class AgentRunner:
                 step_tokens = prompt_tokens + completion_tokens
                 step_cost = result.get("cost", 0)
 
+                # 按需自动创建 root goal:LLM 有 tool 调用但未主动创建目标时兜底
+                if goal_tree and not goal_tree.goals and tool_calls:
+                    has_goal_call = any(
+                        tc.get("function", {}).get("name") == "goal"
+                        for tc in tool_calls
+                    )
+                    if not has_goal_call:
+                        root_desc = goal_tree.mission[:200] if len(goal_tree.mission) > 200 else goal_tree.mission
+                        goal_tree.add_goals(
+                            descriptions=[root_desc],
+                            reasons=["系统自动创建:Agent 未显式创建目标"],
+                            parent_id=None
+                        )
+                        goal_tree.focus(goal_tree.goals[0].id)
+                        if self.trace_store:
+                            await self.trace_store.update_goal_tree(trace_id, goal_tree)
+                            await self.trace_store.add_goal(trace_id, goal_tree.goals[0])
+                        logger.info(f"自动创建 root goal: {goal_tree.goals[0].id}")
+
                 # 获取当前 goal_id
                 current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
 
@@ -564,6 +583,9 @@ class AgentRunner:
                     })
 
                     for tc in tool_calls:
+                        # 每次工具执行前重新获取最新的 goal_id(处理并行 tool_calls 的情况)
+                        current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
+
                         tool_name = tc["function"]["name"]
                         tool_args = tc["function"]["arguments"]
 

+ 26 - 28
agent/memory/skills/core.md

@@ -12,65 +12,63 @@ description: 核心系统能力,自动加载到 System Prompt
 
 ## 计划与执行
 
-对于复杂任务,你要先分析需求,并使用 `goal` 工具管理执行计划和进度这一工具会形成一棵目标树。
+使用 `goal` 工具管理执行计划。目标树是你的工作记忆——系统会定期将当前计划注入给你,帮助你追踪进度和关键结论
 
-### 创建计划:拆分任务步骤,创建TODO
+### 核心原则
+
+- **先明确目标再行动**:开始执行前,用 `goal` 明确当前要做什么
+- **灵活运用,不受约束**:
+  - 可以先做全局规划再行动:`goal(add="调研方案, 实现方案, 测试验证")`
+  - 可以走一步看一步,每次只规划下一个目标
+  - 行动中可以动态放弃并调整:`goal(abandon="方案不可行")`
+  - 规划本身可以作为一个目标(如 "调研并确定技术方案")
+- **简单任务只需一个目标**:`goal(add="将CSV转换为JSON")` 即可,不需要强制拆分
+
+### 使用方式
+
+创建目标:
 
 ```
 goal(add="调研并确定方案, 执行方案, 评估结果")
 ```
 
-这将创建3个目标。你可以在执行过程中继续添加子目标。
-
-### 开始执行
-
-聚焦到某个目标开始执行(使用目标的 ID):
+聚焦并开始执行(使用计划视图中的 ID,如 "1", "2.1"):
 
 ```
 goal(focus="1")
 ```
 
-目标的 ID 会显示在计划视图中,格式如 "1", "2", "2.1", "2.2" 等。
-
-### 完成目标
-
-完成当前目标并提供总结:
+完成目标,记录**关键结论**(不是过程描述):
 
 ```
-goal(done="人物姿势的最佳提取工具是openpose")
+goal(done="最佳方案是openpose,精度高且支持多人检测")
 ```
 
-### 完成并切换
-
-先完成当前目标,再切换焦点到下一个:
+完成并切换到下一个:
 
 ```
-goal(done="人物姿势的最佳提取工具是openpose", focus="2")
+goal(done="openpose方案确认可行", focus="2")
 ```
 
-这会先完成当前正在执行的目标,然后切换焦点到目标 "2"。
-
-### 添加子目标
-
-为指定目标添加子目标:
+添加子目标或同级目标:
 
 ```
 goal(add="设计接口, 实现代码", under="2")
+goal(add="编写文档", after="2")
 ```
 
-在指定目标后面添加同级目标:
+放弃不可行的目标:
 
 ```
-goal(add="编写文档", after="2")
+goal(abandon="方案A需要Redis,环境没有")
 ```
 
 ### 使用规范
 
 1. **同时只有一个目标处于执行中**:完成当前目标后再切换
-2. **summary 应简洁**:记录关键结论和发现,不要冗长
-3. **计划可调整**:根据执行情况追加或跳过目标
-4. **简单任务不需要计划**:单步操作直接执行即可
-5. **使用 ID 进行定位**:focus、after、under 参数都使用目标的 ID(如 "1", "2.1")
+2. **summary 记录结论**:记录关键发现,而非 "已完成调研" 这样无信息量的描述
+3. **计划可调整**:根据执行情况随时追加、跳过或放弃目标
+4. **使用 ID 定位**:focus、after、under 参数使用目标的 ID(如 "1", "2.1")
 
 ## 信息调研
 

+ 16 - 7
agent/tools/builtin/subagent.py

@@ -247,7 +247,10 @@ async def _handle_explore_mode(
     for i, branch in enumerate(branches):
         # 生成唯一的 sub_trace_id
         sub_trace_id = generate_sub_trace_id(current_trace_id, f"explore-{i+1:03d}")
-        sub_trace_ids.append(sub_trace_id)
+        sub_trace_ids.append({
+            "trace_id": sub_trace_id,
+            "mission": branch
+        })
 
         # 创建 Sub-Trace
         parent_trace = await store.get_trace(current_trace_id)
@@ -301,13 +304,13 @@ async def _handle_explore_mode(
             }
             processed_results.append(error_result)
             await broadcast_sub_trace_completed(
-                current_trace_id, sub_trace_ids[i],
+                current_trace_id, sub_trace_ids[i]["trace_id"],
                 "failed", str(result), {}
             )
         else:
             processed_results.append(result)
             await broadcast_sub_trace_completed(
-                current_trace_id, sub_trace_ids[i],
+                current_trace_id, sub_trace_ids[i]["trace_id"],
                 result.get("status", "completed"),
                 result.get("summary", ""),
                 result.get("stats", {})
@@ -350,7 +353,10 @@ async def _handle_delegate_mode(
         if not existing_trace:
             return {"status": "failed", "error": f"Continue-from trace not found: {continue_from}"}
         sub_trace_id = continue_from
-        sub_trace_ids = [sub_trace_id]
+        # 获取 mission
+        goal_tree = await store.get_goal_tree(continue_from)
+        mission = goal_tree.mission if goal_tree else task
+        sub_trace_ids = [{"trace_id": sub_trace_id, "mission": mission}]
     else:
         parent_trace = await store.get_trace(current_trace_id)
         sub_trace_id = generate_sub_trace_id(current_trace_id, "delegate")
@@ -369,7 +375,7 @@ async def _handle_delegate_mode(
         )
         await store.create_trace(sub_trace)
         await store.update_goal_tree(sub_trace_id, GoalTree(mission=task))
-        sub_trace_ids = [sub_trace_id]
+        sub_trace_ids = [{"trace_id": sub_trace_id, "mission": task}]
 
         # 广播 sub_trace_started
         await broadcast_sub_trace_started(
@@ -458,7 +464,10 @@ async def _handle_evaluate_mode(
         if not existing_trace:
             return {"status": "failed", "error": f"Continue-from trace not found: {continue_from}"}
         sub_trace_id = continue_from
-        sub_trace_ids = [sub_trace_id]
+        # 获取 mission
+        goal_tree = await store.get_goal_tree(continue_from)
+        mission = goal_tree.mission if goal_tree else task_prompt
+        sub_trace_ids = [{"trace_id": sub_trace_id, "mission": mission}]
     else:
         parent_trace = await store.get_trace(current_trace_id)
         sub_trace_id = generate_sub_trace_id(current_trace_id, "evaluate")
@@ -477,7 +486,7 @@ async def _handle_evaluate_mode(
         )
         await store.create_trace(sub_trace)
         await store.update_goal_tree(sub_trace_id, GoalTree(mission=task_prompt))
-        sub_trace_ids = [sub_trace_id]
+        sub_trace_ids = [{"trace_id": sub_trace_id, "mission": task_prompt}]
 
         # 广播 sub_trace_started
         await broadcast_sub_trace_started(

+ 1 - 1
agent/trace/goal_models.py

@@ -62,7 +62,7 @@ class Goal:
     summary: Optional[str] = None            # 完成/放弃时的总结
 
     # agent_call 特有
-    sub_trace_ids: Optional[List[str]] = None      # 启动的 Sub-Trace IDs
+    sub_trace_ids: Optional[List[Dict[str, str]]] = None      # 启动的 Sub-Trace 信息 [{"trace_id": "...", "mission": "..."}]
     agent_call_mode: Optional[str] = None          # "explore" | "delegate" | "sequential"
     sub_trace_metadata: Optional[Dict[str, Dict[str, Any]]] = None  # Sub-Trace 元数据
 

+ 13 - 7
agent/trace/goal_tool.py

@@ -247,15 +247,21 @@ def create_goal_tool_schema() -> dict:
     """创建 goal 工具的 JSON Schema"""
     return {
         "name": "goal",
-        "description": """管理执行计划。
+        "description": """管理执行计划。目标工具是灵活的支持系统,帮助你组织和追踪工作进度。
 
+使用策略(按需选择):
+- 全局规划:先规划所有目标,再逐个执行
+- 渐进规划:走一步看一步,每次只创建下一个目标
+- 动态调整:行动中随时 abandon 不可行的目标,创建新目标
+
+参数:
 - add: 添加目标(逗号分隔多个)
-- reason: 创建理由(逗号分隔多个,与 add 一一对应)。说明为什么要做这些目标。
-- after: 在指定目标后面添加(同层级)。使用目标的 ID。
-- under: 为指定目标添加子目标。使用目标的 ID。如已有子目标,追加到最后。
-- done: 完成当前目标,值为 summary
-- abandon: 放弃当前目标,值为原因(会触发 context 压缩)
-- focus: 切换焦点到指定目标。使用目标 ID。
+- reason: 创建理由(逗号分隔,与 add 一一对应)
+- after: 在指定目标后面添加同级目标。使用目标 ID。
+- under: 为指定目标添加子目标。使用目标 ID。如已有子目标,追加到最后。
+- done: 完成当前目标,值为 summary(记录关键结论)
+- abandon: 放弃当前目标,值为原因
+- focus: 切换焦点到指定目标。使用目标 ID。
 
 位置控制(优先使用 after):
 - 不指定 after/under: 添加到当前 focus 下作为子目标(无 focus 时添加到顶层)

+ 14 - 5
docs/README.md

@@ -129,21 +129,29 @@ async def run(task: str, agent_type: str = "default") -> AsyncIterator[Union[Tra
 
     # 4. ReAct 循环
     for step in range(max_iterations):
-        # 注入当前计划
-        plan_text = goal_tree.to_prompt()
+        # 注入当前计划(如果有 goals)
+        if goal_tree.goals:
+            inject_plan(goal_tree.to_prompt())
 
         # 调用 LLM
         response = await llm.chat(
             messages=messages,
-            system=system_prompt + plan_text,
+            system=system_prompt,
             tools=tool_registry.to_schema()
         )
 
-        # 记录 assistant Message
+        # 按需自动创建 root goal:LLM 有 tool 调用但未主动创建目标时兜底
+        if not goal_tree.goals and response.tool_calls:
+            if "goal" not in [tc.name for tc in response.tool_calls]:
+                goal_tree.add_goals([mission[:200]])
+                goal_tree.focus(goal_tree.goals[0].id)
+
+        # 记录 assistant Message(goal_id = goal_tree.current_id)
         await store.add_message(Message.create(
             trace_id=trace.trace_id,
             role="assistant",
             sequence=next_seq,
+            goal_id=goal_tree.current_id,
             content=response
         ))
         yield assistant_msg
@@ -159,6 +167,7 @@ async def run(task: str, agent_type: str = "default") -> AsyncIterator[Union[Tra
                 trace_id=trace.trace_id,
                 role="tool",
                 sequence=next_seq,
+                goal_id=goal_tree.current_id,
                 content=result
             ))
             yield tool_msg
@@ -297,7 +306,7 @@ class Message:
     trace_id: str
     role: Literal["system", "user", "assistant", "tool"]
     sequence: int                            # 全局顺序
-    goal_id: Optional[str] = None            # 关联的 Goal ID
+    goal_id: Optional[str] = None            # 关联的 Goal ID(初始消息为 None,系统会按需自动创建 root goal 兜底)
     description: str = ""                    # 系统自动生成的摘要
     tool_call_id: Optional[str] = None
     content: Any = None

+ 45 - 0
docs/decisions.md

@@ -667,3 +667,48 @@ execution trace v2.0 引入了 Blob 存储系统用于处理大输出和图片
 
 ---
 
+## Goal 按需自动创建
+
+**日期**: 2026-02-10
+
+### 问题
+
+Agent(含 sub-agent)有时不创建 goal 就直接执行工具调用,导致 message 的 goal_id 为 null。这造成:
+1. 统计信息丢失(`_update_goal_stats` 跳过 null goal_id)
+2. 可视化缺失结构(前端降级为合成 "START" 节点)
+3. LLM 缺少 context 锚点(goals 为空时不注入计划)
+
+### 方案对比
+
+| 方案 | 优点 | 缺点 |
+|------|------|------|
+| **预创建 root goal** | 保证非 null | 复杂任务多一层无意义嵌套,需要溶解逻辑 |
+| **全面接受 null** | 无改动 | 丢失统计、可视化、context 锚点 |
+| **按需自动创建** | 仅在需要时兜底,不干扰正常规划 | 首轮含 goal() 调用时该轮消息仍为 null(可接受) |
+
+### 决策
+
+**选择:按需自动创建 + prompt 引导**
+
+**触发条件**(三个 AND):
+1. `goal_tree.goals` 为空(尚无任何目标)
+2. LLM 返回了 `tool_calls`(正在执行操作)
+3. `tool_calls` 中不包含 `goal()` 调用(LLM 未自行创建目标)
+
+**触发时机**:LLM 返回后、记录消息前(`runner.py` agent loop 中)
+
+**行为**:从 `goal_tree.mission` 截取前 200 字符作为 root goal description,创建并 focus。
+
+**Prompt 配合**:`core.md` 引导 LLM "先明确目标再行动",但不强制。
+
+**实现**:`agent/core/runner.py:AgentRunner.run`
+
+### 理由
+
+1. **不干扰 LLM 自主规划**:LLM 创建目标时,树结构完全由 LLM 控制,无多余嵌套
+2. **兜底覆盖遗漏**:LLM 跳过目标直接行动时,系统自动补位
+3. **实现简单**:无需 `is_auto_root` 标记、溶解逻辑或 display ID 特殊处理
+4. **可接受的 gap**:首轮含 `goal()` 调用时该轮消息 goal_id 为 null,仅影响一轮,属于规划阶段的过渡消息
+
+---
+

+ 17 - 5
frontend/API.md

@@ -139,7 +139,10 @@ GET /api/traces/{trace_id}
         "reason": "评估不同技术选型",
         "status": "in_progress",
         "agent_call_mode": "explore",
-        "sub_trace_ids": ["abc123.A", "abc123.B"],
+        "sub_trace_ids": [
+          {"trace_id": "abc123.A", "mission": "JWT 方案"},
+          {"trace_id": "abc123.B", "mission": "Session 方案"}
+        ],
         "sub_trace_metadata": null,
         "self_stats": { "message_count": 0, "total_tokens": 0, "total_cost": 0.0, "preview": null },
         "cumulative_stats": { "message_count": 0, "total_tokens": 0, "total_cost": 0.0, "preview": null }
@@ -208,8 +211,14 @@ GET /api/traces/{trace_id}
         "status": "completed",
         "agent_call_mode": "explore",
         "sub_trace_ids": [
-          "abc123@explore-20260204220012-001",
-          "abc123@explore-20260204220012-002"
+          {
+            "trace_id": "abc123@explore-20260204220012-001",
+            "mission": "JWT 方案"
+          },
+          {
+            "trace_id": "abc123@explore-20260204220012-002",
+            "mission": "Session 方案"
+          }
         ],
         "sub_trace_metadata": {
           "abc123@explore-20260204220012-001": {
@@ -692,7 +701,7 @@ if (data.event === 'sub_trace_completed') {
 | `reason` | string | 创建理由(为什么做)|
 | `status` | string | `pending` / `in_progress` / `completed` / `abandoned` |
 | `summary` | string \| null | 完成/放弃时的总结 |
-| `sub_trace_ids` | string[] \| null | 启动的 Sub-Trace IDs(仅 agent_call)|
+| `sub_trace_ids` | Array<{trace_id: string, mission: string}> \| null | 启动的 Sub-Trace 信息(仅 agent_call)|
 | `agent_call_mode` | string \| null | "explore" / "delegate" / "sequential"(仅 agent_call)|
 | `sub_trace_metadata` | object \| null | Sub-Trace 元数据(仅 agent_call,包含最后消息等)|
 | `self_stats` | GoalStats | 自身统计 |
@@ -1078,7 +1087,10 @@ const mainTrace = {
   goal_tree: {
     goals: [
       { id: "1", type: "normal", description: "分析问题" },
-      { id: "2", type: "agent_call", agent_call_mode: "explore", sub_trace_ids: ["abc123.A", "abc123.B"] },
+      { id: "2", type: "agent_call", agent_call_mode: "explore", sub_trace_ids: [
+        {"trace_id": "abc123.A", "mission": "JWT 方案"},
+        {"trace_id": "abc123.B", "mission": "Session 方案"}
+      ] },
       { id: "3", type: "normal", description: "完善实现" }
     ]
   },