elksmmx 6 дней назад
Родитель
Сommit
3cb3cb714b

+ 40 - 9
agent/core/runner.py

@@ -864,6 +864,16 @@ class AgentRunner:
 
             if self.trace_store:
                 await self.trace_store.add_message(assistant_msg)
+                # 记录模型使用
+                await self.trace_store.record_model_usage(
+                    trace_id=trace_id,
+                    sequence=sequence - 1,  # assistant_msg的sequence
+                    role="assistant",
+                    model=config.model,
+                    prompt_tokens=prompt_tokens,
+                    completion_tokens=completion_tokens,
+                    cache_read_tokens=cache_read_tokens or 0,
+                )
 
             yield assistant_msg
             head_seq = sequence
@@ -927,12 +937,21 @@ class AgentRunner:
                     )
 
                     # --- 支持多模态工具反馈 ---
-                    # execute() 返回 dict{"text","images"} 或 str
-                    if isinstance(tool_result, dict) and tool_result.get("images"):
-                        tool_result_text = tool_result["text"]
+                    # execute() 返回 dict{"text","images","tool_usage"} 或 str
+                    # 统一为dict格式
+                    if isinstance(tool_result, str):
+                        tool_result = {"text": tool_result}
+
+                    tool_text = tool_result.get("text", str(tool_result))
+                    tool_images = tool_result.get("images", [])
+                    tool_usage = tool_result.get("tool_usage")  # 新增:提取tool_usage
+
+                    # 处理多模态消息
+                    if tool_images:
+                        tool_result_text = tool_text
                         # 构建多模态消息格式
-                        tool_content_for_llm = [{"type": "text", "text": tool_result_text}]
-                        for img in tool_result["images"]:
+                        tool_content_for_llm = [{"type": "text", "text": tool_text}]
+                        for img in tool_images:
                             if img.get("type") == "base64" and img.get("data"):
                                 media_type = img.get("media_type", "image/png")
                                 tool_content_for_llm.append({
@@ -944,8 +963,8 @@ class AgentRunner:
                         img_count = len(tool_content_for_llm) - 1  # 减去 text 块
                         print(f"[Runner] 多模态工具反馈: tool={tool_name}, images={img_count}, text_len={len(tool_result_text)}")
                     else:
-                        tool_result_text = str(tool_result)
-                        tool_content_for_llm = tool_result_text
+                        tool_result_text = tool_text
+                        tool_content_for_llm = tool_text
 
                     tool_msg = Message.create(
                         trace_id=trace_id,
@@ -960,10 +979,22 @@ class AgentRunner:
 
                     if self.trace_store:
                         await self.trace_store.add_message(tool_msg)
+                        # 记录工具的模型使用
+                        if tool_usage:
+                            await self.trace_store.record_model_usage(
+                                trace_id=trace_id,
+                                sequence=sequence,
+                                role="tool",
+                                tool_name=tool_name,
+                                model=tool_usage.get("model"),
+                                prompt_tokens=tool_usage.get("prompt_tokens", 0),
+                                completion_tokens=tool_usage.get("completion_tokens", 0),
+                                cache_read_tokens=tool_usage.get("cache_read_tokens", 0),
+                            )
                         # 截图单独存为同名 PNG 文件
-                        if isinstance(tool_result, dict) and tool_result.get("images"):
+                        if tool_images:
                             import base64 as b64mod
-                            for img in tool_result["images"]:
+                            for img in tool_images:
                                 if img.get("data"):
                                     png_path = self.trace_store._get_messages_dir(trace_id) / f"{tool_msg.message_id}.png"
                                     png_path.write_bytes(b64mod.b64decode(img["data"]))

+ 3 - 0
agent/tools/builtin/__init__.py

@@ -19,6 +19,7 @@ from agent.tools.builtin.experience import get_experience
 from agent.tools.builtin.search import search_posts, get_search_suggestions
 from agent.tools.builtin.sandbox import (sandbox_create_environment, sandbox_run_shell,
                                          sandbox_rebuild_with_ports,sandbox_destroy_environment)
+from agent.trace.goal_tool import goal
 
 # 导入浏览器工具以触发注册
 import agent.tools.builtin.browser  # noqa: F401
@@ -45,4 +46,6 @@ __all__ = [
     "sandbox_run_shell",
     "sandbox_rebuild_with_ports",
     "sandbox_destroy_environment",
+    # 计划管理
+    "goal",
 ]

+ 3 - 0
agent/tools/models.py

@@ -41,6 +41,9 @@ class ToolResult:
 	attachments: List[str] = field(default_factory=list)  # 文件路径列表
 	images: List[Dict[str, Any]] = field(default_factory=list)  # 图片列表
 
+	# Token追踪(用于工具内部LLM调用)
+	tool_usage: Optional[Dict[str, Any]] = None  # 格式:{"model": "...", "prompt_tokens": 100, "completion_tokens": 50, "cost": 0.0}
+
 	def to_llm_message(self, first_time: bool = True) -> str:
 		"""
 		转换为给 LLM 的消息

+ 13 - 3
agent/tools/registry.py

@@ -241,10 +241,20 @@ class ToolRegistry:
 			# 处理 ToolResult 对象
 			from agent.tools.models import ToolResult
 			if isinstance(result, ToolResult):
-				# 有图片时返回 dict 以便 runner 构建多模态消息
+				ret = {"text": result.to_llm_message()}
+
+				# 保留images
 				if result.images:
-					return {"text": result.to_llm_message(), "images": result.images}
-				return result.to_llm_message()
+					ret["images"] = result.images
+
+				# 保留tool_usage
+				if result.tool_usage:
+					ret["tool_usage"] = result.tool_usage
+
+				# 向后兼容:只有text时返回字符串
+				if len(ret) == 1:
+					return ret["text"]
+				return ret
 
 			return json.dumps(result, ensure_ascii=False, indent=2)
 

+ 102 - 0
agent/trace/store.py

@@ -61,6 +61,10 @@ class FileSystemTraceStore:
         """获取 events.jsonl 文件路径"""
         return self._get_trace_dir(trace_id) / "events.jsonl"
 
+    def _get_model_usage_file(self, trace_id: str) -> Path:
+        """获取 model_usage.json 文件路径"""
+        return self._get_trace_dir(trace_id) / "model_usage.json"
+
     # ===== Trace 操作 =====
 
     async def create_trace(self, trace: Trace) -> str:
@@ -566,6 +570,104 @@ class FileSystemTraceStore:
 
         return abandoned_ids
 
+    # ===== 模型使用追踪 =====
+
+    async def record_model_usage(
+        self,
+        trace_id: str,
+        sequence: int,
+        role: str,
+        model: str,
+        prompt_tokens: int,
+        completion_tokens: int,
+        cache_read_tokens: int = 0,
+        tool_name: Optional[str] = None,
+    ) -> None:
+        """
+        记录模型使用情况到 model_usage.json
+
+        Args:
+            trace_id: Trace ID
+            sequence: 消息序号
+            role: 角色(assistant/tool)
+            model: 模型名称
+            prompt_tokens: 输入tokens
+            completion_tokens: 输出tokens
+            cache_read_tokens: 缓存读取tokens
+            tool_name: 工具名称(role=tool时)
+        """
+        usage_file = self._get_model_usage_file(trace_id)
+
+        # 读取现有数据
+        if usage_file.exists():
+            data = json.loads(usage_file.read_text())
+        else:
+            data = {
+                "summary": {
+                    "total_models": 0,
+                    "total_tokens": 0,
+                    "total_cache_read_tokens": 0,
+                    "agent_tokens": 0,
+                    "tool_tokens": 0,
+                },
+                "models": [],
+                "timeline": [],
+            }
+
+        # 更新summary
+        total_tokens = prompt_tokens + completion_tokens
+        data["summary"]["total_tokens"] += total_tokens
+        data["summary"]["total_cache_read_tokens"] += cache_read_tokens
+
+        if role == "assistant":
+            data["summary"]["agent_tokens"] += total_tokens
+            source = "agent"
+        else:
+            data["summary"]["tool_tokens"] += total_tokens
+            source = f"tool:{tool_name}" if tool_name else "tool"
+
+        # 更新models列表
+        model_entry = None
+        for m in data["models"]:
+            if m["model"] == model and m["source"] == source:
+                model_entry = m
+                break
+
+        if model_entry:
+            model_entry["prompt_tokens"] += prompt_tokens
+            model_entry["completion_tokens"] += completion_tokens
+            model_entry["total_tokens"] += total_tokens
+            model_entry["cache_read_tokens"] += cache_read_tokens
+            model_entry["call_count"] += 1
+        else:
+            data["models"].append({
+                "model": model,
+                "source": source,
+                "prompt_tokens": prompt_tokens,
+                "completion_tokens": completion_tokens,
+                "total_tokens": total_tokens,
+                "cache_read_tokens": cache_read_tokens,
+                "call_count": 1,
+            })
+            data["summary"]["total_models"] = len(data["models"])
+
+        # 添加到timeline
+        timeline_entry = {
+            "sequence": sequence,
+            "role": role,
+            "model": model,
+            "prompt_tokens": prompt_tokens,
+            "completion_tokens": completion_tokens,
+        }
+        if cache_read_tokens > 0:
+            timeline_entry["cache_read_tokens"] = cache_read_tokens
+        if tool_name:
+            timeline_entry["tool_name"] = tool_name
+        data["timeline"].append(timeline_entry)
+
+        # 写回文件
+        usage_file.write_text(json.dumps(data, indent=2, ensure_ascii=False))
+
     # ===== 事件流操作(用于 WebSocket 断线续传)=====
 
     async def get_events(

+ 208 - 33
examples/find knowledge/test.prompt

@@ -4,40 +4,215 @@ temperature: 0.3
 ---
 
 $system$
-你是最顶尖的AI助手,擅长图像分析和特征提取研究。你可以调用工具逐步解决复杂问题
+你是面向可逆特征建模的多模态分析专家。你的核心目标是:构建可逆的多模态特征空间,使生成模型能够基于特征重建原始图片。生成模型可以是任何AI模型或工具
 
 $user$
-我的任务是:从一组图片中提取特征,构建一个可逆的特征空间,再从这些特征还原得到原先的图片组,以验证特征的有效性。
-目前的问题:特征以制作表和亮点数据的形式给出,但制作表只包含文字信息,有一些特征需要用多模态的特征,但我们无法筛选出需要的维度。
-我需要你帮我:
-    1.根据输入的原始图片、制作表和得到的亮点 JSON 数据,判断需要哪些多模态维度
-    2.根据得到的维度,提取到具体的值,更新制作表(不需要完整更新,只需要将得到的特征值和制作表关联即可)
-多模态特征维度(Multimodal Feature Dimension)是指图像在某一独立信息子空间中的结构化投影表示。
-- 该表示:不包含原始像素,可由算法或模型等工具提取,可独立存储,可与其他维度组合,在理论上可参与图像重构。最终目标是根据多模态特征还原图片
-- 比如:几何结构表示(如三视图、深度结构等)、颜色统计表示(如色彩分布向量)、边缘与轮廓表示(如线性结构图)**仅仅作为参考,具体情况应具体分析
-- **特征表示形式**:可以是数值形式(JSON)或图像形式(PNG/NPY),如深度图、边缘图、分割图、法线图等
-
-**还原过程说明**:
-- 最终,负责还原的agent将获得更新的制作表,包括多模态维度和值,以生成式模型为主,从特征还原图片
+# 任务目标
+
+从 `input/` 目录中分析:
+- 原始图片
+- 制作表(包含"实质/形式"结构)
+- 亮点 JSON 数据
+- 制作点数据(包含实质结果,记录了图片组中反复出现的元素)
+
+**核心目的**:筛选并提取多模态特征维度,使其成为生成模型友好的控制信号。这些特征不仅用于还原图像,更重要的是用于学习、复用和建构全新内容。
+
+---
+
+# 一、核心概念
+
+## 1. 多模态特征维度
+
+多模态特征维度是图像在某一独立信息子空间中的表示:
+- 不包含原始像素
+- 可由专业工具提取
+- 可独立存储和组合
+- 理论上可参与图像重构
+- **必须是生成模型友好的控制信号**
+
+**表示形式**:
+- 数值形式:JSON
+- 图像形式:PNG/NPY
+- 语言形式:自然语言的描述
+
+## 2. 实质/形式双层模型
+
+所有特征必须明确归属为"实质"或"形式":
+
+**实质(Substance)**:
+- 定义:图像中的某一个物体本身
+- 例如:一个人物、一个建筑、一个物品
+- 制作点实质结果中记录了图片组中多次出现的重要实质
+
+**形式(Form)**:
+- 定义:实质的各种属性,或图像整体的属性
+- 作用于实质的形式:物体的颜色、姿态、材质、光照等
+- 作用于图像整体的形式:构图、整体色调、风格等
+- 注意:即使某个形式(如构图)不属于任何具体实质,如果需要也要提取
+
+**基本规则**:先识别实质(物体本身),再推导形式(物体的属性)。
+
+---
+
+# 二、工作流程
+
+## 第一步:筛选维度
+
+### 1. 分析输入数据
+- 查看原始图片,理解图片组的整体特征
+- 阅读制作表,理解实质/形式结构
+- **重点关注亮点数据**:亮点是图片表现力的核心
+- **重点关注制作点实质结果**:记录了图片组中反复出现的元素
+
+### 2. 识别实质
+- 确认核心实质(图片中的物体本身)
+- **制作点实质结果中反复出现的元素具有优先级**:这些元素本身就是具有一致性要求的实质
+- 输出实质列表
+
+### 3. 推导或匹配形式
+- 为每个实质推导或匹配对应的形式(与制作表/亮点进行匹配)
+- 识别图像整体的形式(如构图),如果对还原有帮助,即使不属于具体实质也要考虑
+- 输出形式列表
+
+### 4. 搜索还原经验
+- 搜索其他人使用生成模型还原图像的经验,并保存在knowledge中
+- 了解哪些特征维度对生成模型更友好
+- 评估搜索结果,如果不够好需要调整关键词继续搜索
+- 广泛收集信息,目标平台尽可能多,知识有相关性即可保存,用于指导之后的维度筛选
+- 将研究过程和发现保存在 `knowledge/restoration_experience/` 目录,保留原始URL
+
+### 5. 筛选多模态维度
+- 为每个实质筛选合适的多模态维度
+- 为每个形式筛选合适的多模态维度
+- 优先选择可逆性强、生成模型友好的维度
+- **前瞻性思考**:筛选时就要考虑每个特征在还原中如何被使用、起到什么作用
+- **避免过度相似**:不要提取与原图过于相似的特征(如深度图),因为为了还原而还原没有价值,特征应该能用于学习、复用和建构全新内容
+
+## 第二步:提取特征值
+
+### 1. 知识研究
+
+**搜索工具**:
+- 在内容平台广泛搜索专业工具
+- 可以先大量地搜索相关知识后筛选
+- 深入研究工具使用方法,不要浅尝辄止
+- 根据搜索结果评估query关键词,如果不够好需要调整关键词继续搜索
+- 将研究过程和发现保存在 `knowledge/tools/` 目录,保留原始URL
+
+### 2. 工具选择
+
+**评估标准**:
+- 发布时间:优先近期更新的工具(建议先确定当前时间,再判断工具是否近期更新)
+- 是否支持多模态处理
+- 是否支持批量处理
+- 是否支持API或可编程调用
+
+**选择建议**:
+- 优先选择更新、更通用、更多人使用或推荐的工具
+
+### 3. 特征提取
+
+**提取过程**:
+- 使用专业工具提取特征值
+- 每个维度单独建立文件夹:`output/features/维度名称/`
+
+**文件组织**:
+- 特征值文件(.png 或 .json)
+- mapping.json(记录维度与制作表的对应关系)
+
+**mapping.json 格式示例**:
+```json
+{
+  "dimension": "depth_map",
+  "mappings": [
+    {
+      "file": "img_1_segment_1.png",
+      "source_image": "input/img_1.jpg",
+      "segment": 1,
+      "category": "实质",
+      "feature": "空间深度结构"
+    }
+  ]
+}
+```
+
+**对应关系**:
+- 特征值必须与制作表精确对应
+- **必须与特定的一个或几个特征关联**,不能模糊处理,更不能只关联到亮点
+- **根据真实key串联完整路径**:从段落 → ... → 最后一层特征,确定提取到的多模态特征值属于谁
+- 如果是实质,直接关联到段落本身
+
+### 4. 输出研究报告
+- 总结筛选了哪些多模态维度及原因
+- **明确每个特征在还原中如何被使用、起到什么作用**
+- 说明每个特征的可逆性和重建价值
+- 说明每个特征如何用于学习、复用和建构全新内容
+- 记录工具选择理由和使用经验
+- 单独生成一份报告,记录每次搜索的关键词、策略,以及得到的结果
+
+---
+
+# 三、核心原则
+
+## 解构原则
+
+**亮点驱动**:
+- 亮点数据是图片表现力的核心
+- 筛选维度时重点参考亮点
+- 对高权重段落细致处理
+
+**可逆性优先**:
+- 优先选择可逆性强的维度
 - 特征应该是生成模型友好的控制信号
+- 避免信息损失过大的表示
+- **避免提取与原图过于相似的特征**:特征应该是抽象的、可复用的,而不是原图的复制
+
+**价值导向**:
+- 特征不仅用于还原,更要用于学习、复用和建构全新内容
+- 为了还原而还原没有价值
+- 优先提取具有泛化能力和创造性价值的特征
+
+**适度解构**:
+- 维度数量适中(建议6-10个)
+- 避免过度细分或过度简化
+- 根据图片组的复杂度灵活调整
+
+**复用优先**:
+- 若已有维度可以表达目标语义,不新增维度
+- 新维度必须给出必要性说明
+
+**一致性保证**(针对图片组):
+- 若图片组中存在重复实质,保持一致的表示方式
+- 例如:相同骨架比例、相同主色调范围、相同空间比例关系
+- 一致性优先级高于创意优先级
+
+## 质量要求
+
+**禁止降级解决**:
+- 不允许为了方便而使用效果显著更差的简单方案
+
+**禁止平凡表示**:
+- 不允许只提供自然语言描述
+- 必须使用多模态提供超越语言的信息
+
+**禁止保存原始图片**:
+- 图片裁剪只能作为中间步骤
+- 最终必须提取多模态特征
+
+---
+
+# 四、还原与创造说明
+
+最终,负责还原的agent将获得:
+- 更新的制作表(包含多模态维度和值)
+- 各维度的特征文件
+
+还原agent将以生成式模型为主,使用这些特征作为控制信号重建图片。
+
+**更重要的是**:这些特征不仅用于还原原图,更要用于学习规律、复用特征、建构全新内容。因此,特征应该具有泛化能力和创造性价值,而不是原图的简单复制。
+
+---
+
+# 开始执行
 
-**关键要求**:不能保存图片本身。图片裁剪只能作为中间步骤,最终必须从裁剪区域提取多模态特征。
-
-要求:
-- 分析 input/ 目录下的原始图片、制作表和对应的亮点 JSON 数据
-- 亮点是图片表现力的核心,筛选的维度应该重点参考亮点数据
-- 判断和筛选过程以先验知识作为支撑,应该在内容平台进行广泛的搜索,要深入研究,不要浅尝辄止
-- 将研究过程和发现保存在 knowledge/ 目录下,注意保留原始来源的 URL
-- 最终输出一份完整的研究报告,总结应该提取哪些多模态维度,以及为什么
-- **特征提取工具链**:必须使用专业工具从图片中提取多模态特征维度的值
-  - 搜索并学习相关工具的使用方法
-  - 使用最合适的工具提取特征
-- **特征文件组织结构**:每个维度单独建一个文件夹(output/features/维度名称/)
-  - 文件夹内包含:
-    - 具体的特征值文件(图片格式如 .png 或数值格式如 .json)
-    - mapping.json 文件,记录该维度与制作表的对应关系(哪个图片、哪个段落、实质/形式、哪个特征)
-  - 示例结构:output/features/depth_map/img_1_segment_1.png 和 output/features/depth_map/mapping.json。文件内部应包括:多模态特征维度-维度值:图片-段落-实质/形式-特征(包含整条路径)
-- 筛选得到的多模态值需要与制作表对应,具体到某一个段落,实质或形式下的具体特征。
-- 可以参考制作表和亮点的权重,对于权重高的段落,应该细致处理
-- **禁止降级解决**:不允许为了方便而使用效果显著更差的简单方案
-- **禁止平凡表示**:不允许只提供自然语言的特征表示,而是应该使用多模态提供超越语言的信息。
+请根据上述原则,灵活分析 `input/` 目录下的数据,完成多模态特征的筛选和提取工作。