Преглед на файлове

Merge branch 'lzh_knowledge_1202' of https://git.yishihui.com/yangxiaohui/kg_agent into lzh_knowledge_1202

liuzhiheng преди 5 часа
родител
ревизия
15407308fa
променени са 23 файла, в които са добавени 761 реда и са изтрити 687 реда
  1. 23 34
      knowledge_v2/.cache/9f510b2a8348/execution_record.json
  2. 25 0
      knowledge_v2/.cache/9f510b2a8348/function_knowledge/execution_detail.json
  3. 0 48
      knowledge_v2/.cache/9f510b2a8348/function_knowledge/final_result.json
  4. 1 1
      knowledge_v2/.cache/9f510b2a8348/function_knowledge/generated_query.txt
  5. 1 0
      knowledge_v2/.cache/9f510b2a8348/function_knowledge/selected_tool.txt
  6. 3 0
      knowledge_v2/.cache/9f510b2a8348/function_knowledge/tool_params.json
  7. 2 0
      knowledge_v2/.cache/9f510b2a8348/function_knowledge/tool_result.json
  8. 0 26
      knowledge_v2/.cache/9f510b2a8348/knowledge.txt
  9. 0 6
      knowledge_v2/.cache/9f510b2a8348/llm_search/generated_queries.json
  10. 0 48
      knowledge_v2/.cache/9f510b2a8348/llm_search/merged_knowledge.txt
  11. 0 14
      knowledge_v2/.cache/9f510b2a8348/llm_search/search_results/search_result_001.txt
  12. 0 24
      knowledge_v2/.cache/9f510b2a8348/llm_search/search_results/search_result_002.txt
  13. 0 14
      knowledge_v2/.cache/9f510b2a8348/llm_search/search_results/search_result_003.txt
  14. 0 17
      knowledge_v2/.cache/9f510b2a8348/llm_search/search_results/search_result_004.txt
  15. 0 26
      knowledge_v2/.cache/9f510b2a8348/multi_search/final_knowledge.txt
  16. 0 26
      knowledge_v2/.cache/9f510b2a8348/multi_search/merged_knowledge.txt
  17. 0 115
      knowledge_v2/clear_cache.py
  18. 259 0
      knowledge_v2/execution_collector.py
  19. 183 230
      knowledge_v2/function_knowledge.py
  20. 149 1
      knowledge_v2/llm_search_knowledge.py
  21. 94 1
      knowledge_v2/multi_search_knowledge.py
  22. 0 56
      knowledge_v2/test_detail_info.py
  23. 21 0
      knowledge_v2/tool_infos/wechat_search_article.txt

Файловите разлики са ограничени, защото са твърде много
+ 23 - 34
knowledge_v2/.cache/9f510b2a8348/execution_record.json


+ 25 - 0
knowledge_v2/.cache/9f510b2a8348/function_knowledge/execution_detail.json

@@ -0,0 +1,25 @@
+{
+  "generate_query": {
+    "cached": false,
+    "prompt": "你是一个智能助手。你的任务是根据用户的问题、帖子信息和账号人设信息,生成一个用于搜索或调用工具的查询语句(Query)。\n\n用户问题:\n教资查分这个信息怎么来的\n\n帖子信息:\n发帖时间:2025.11.07\n\n账号人设信息:\n\n\n请分析上述信息,提炼出核心需求,生成一个简洁明了的查询语句。\n只输出查询语句,不要包含任何解释。",
+    "response": "教资查分 官方发布",
+    "query": "教资查分 官方发布"
+  },
+  "select_tool": {
+    "cached": false,
+    "prompt": "你是一个工具选择专家。你的任务是根据用户的查询语句,从给定的工具列表中选择最合适的一个工具。\n\n用户查询:\n教资查分 官方发布\n\n可用工具列表:\n--- Tool: wechat_search_article ---\n{\n    \"name\": \"wechat_search_article\",\n    \"title\": \"WeChat Official Account Article Search\",\n    \"description\": \"Search for articles from WeChat official accounts using keywords. Supports pagination via cursor-based navigation.\",\n    \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"keyword\": {\n                \"type\": \"string\",\n                \"description\": \"Search keyword, required for querying articles\"\n            },\n            \"cursor\": {\n                \"type\": \"string\",\n                \"description\": \"Pagination cursor. Leave empty for first page, use value from previous response for subsequent pages\"\n            }\n        },\n        \"required\": [\n            \"keyword\"\n        ]\n    }\n}\n\n\n\n请判断是否有工具可以解决用户的查询。\n- 如果有,请返回该工具的名称。\n- 如果没有合适的工具,请返回 \"None\"。\n\n只输出工具名称或 \"None\",不要包含任何解释。",
+    "response": "wechat_search_article",
+    "tool_name": "wechat_search_article",
+    "available_tools_count": 1
+  },
+  "extract_params": {
+    "cached": false,
+    "prompt": "你是一个API调用专家。你的任务是根据工具的信息和用户的查询,生成正确的调用参数。\n\n查询内容:\n教资查分 官方发布\n\n工具信息:\n{\n    \"name\": \"wechat_search_article\",\n    \"title\": \"WeChat Official Account Article Search\",\n    \"description\": \"Search for articles from WeChat official accounts using keywords. Supports pagination via cursor-based navigation.\",\n    \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"keyword\": {\n                \"type\": \"string\",\n                \"description\": \"Search keyword, required for querying articles\"\n            },\n            \"cursor\": {\n                \"type\": \"string\",\n                \"description\": \"Pagination cursor. Leave empty for first page, use value from previous response for subsequent pages\"\n            }\n        },\n        \"required\": [\n            \"keyword\"\n        ]\n    }\n}\n\n请分析工具的参数要求,根据查询内容提取或推断出合适的参数值。\n\n输出格式:请以 JSON 格式输出参数字典,例如:\n{\n    \"param1\": \"value1\",\n    \"param2\": \"value2\"\n}\n\n注意事项:\n1. 只输出参数的JSON字典,不要包含任何解释\n2. 参数名必须与工具定义中的参数名完全一致\n3. 参数值要从查询中提取或合理推断\n4. 不要添加工具定义中没有的参数\n5. 如果某个参数无法从查询中获取,使用合理的默认值或省略该参数\n\n只输出JSON,不要包含markdown标记。",
+    "response": "{\n    \"keyword\": \"教资查分 官方发布\"\n}",
+    "params": {
+      "keyword": "教资查分 官方发布"
+    }
+  },
+  "execution_time": 25.939335346221924,
+  "cache_hits": []
+}

Файловите разлики са ограничени, защото са твърде много
+ 0 - 48
knowledge_v2/.cache/9f510b2a8348/function_knowledge/final_result.json


+ 1 - 1
knowledge_v2/.cache/9f510b2a8348/function_knowledge/generated_query.txt

@@ -1 +1 @@
-教资查分信息来源
+教资查分 官方发布

+ 1 - 0
knowledge_v2/.cache/9f510b2a8348/function_knowledge/selected_tool.txt

@@ -0,0 +1 @@
+wechat_search_article

+ 3 - 0
knowledge_v2/.cache/9f510b2a8348/function_knowledge/tool_params.json

@@ -0,0 +1,3 @@
+{
+  "keyword": "教资查分 官方发布"
+}

Файловите разлики са ограничени, защото са твърде много
+ 2 - 0
knowledge_v2/.cache/9f510b2a8348/function_knowledge/tool_result.json


+ 0 - 26
knowledge_v2/.cache/9f510b2a8348/knowledge.txt

@@ -1,26 +0,0 @@
-教资查分的信息主要通过官方渠道发布和查询,以确保准确性和安全性。针对“2025年11月7日”这个时间点,正是下半年笔试成绩发布的典型时期。
-
-以下是关于教资查分信息来源的详细说明:
-
-### 一、成绩发布信息来源与查询渠道
-
-1.  **官方发布与通知**
-    *   **中小学教师资格考试网(中国教育考试网)**:这是最权威的官方网站,网址为`https://ntce.neea.edu.cn`。成绩发布通知会第一时间在此网站的“通知公告”栏目公布。
-    *   **中国教育考试网微信小程序/官方微信公众号**:教育部教育考试院会通过其官方微信小程序或相关官方微信公众号同步推送成绩查询通知。
-    *   **中国教师资格网**:`https://www.jszg.edu.cn` 虽然主要负责认定工作,有时也会转发成绩查询通知,但查询具体分数仍需跳转至“中国教育考试网”。
-
-2.  **成绩查询入口**
-    *   **中国教育考试网**:考生可通过访问`https://ntce.neea.edu.cn`官网首页的“成绩查询”入口,或直接访问成绩查询页面`https://cjcx.neea.edu.cn`进行查询。
-    *   **中国教育考试网微信小程序**:在微信APP中搜索并进入“中国教育考试网”小程序,即可查询成绩。
-
-### 二、成绩发布时间与“2025年11月7日”的关联
-
-教资笔试成绩通常在考试结束后的一个月左右公布。
-
-*   **下半年笔试成绩**:一般在每年的11月上旬发布。
-*   **“2025年11月7日”的具体情况**:根据官方发布惯例及已有信息,2025年下半年的笔试成绩已于**2025年11月7日上午10点**发布。因此,您提到的“2025年11月7日”正是下半年教资笔试成绩的官方公布时间。
-
-### 三、重要提示
-
-*   为保障个人信息安全并获取准确成绩,务必通过上述官方渠道进行成绩查询,避免使用非官方的第三方网站。
-*   建议考生收藏“中国教育考试网”的中小学教师资格考试页面(`https://ntce.neea.edu.cn`),以便及时获取最权威和准确的信息。

+ 0 - 6
knowledge_v2/.cache/9f510b2a8348/llm_search/generated_queries.json

@@ -1,6 +0,0 @@
-[
-  "教资查分官方信息发布渠道",
-  "教资考试成绩查询日期通知方式",
-  "教育部考试中心教资查分公告",
-  "教资查分信息一般在哪里公布"
-]

+ 0 - 48
knowledge_v2/.cache/9f510b2a8348/llm_search/merged_knowledge.txt

@@ -1,48 +0,0 @@
-## 中小学教师资格考试(教资)成绩查询指南
-
-中小学教师资格考试(教资)的成绩查询主要通过官方渠道进行,以确保信息的准确性和安全性。以下是关于教资成绩查询的全面指南:
-
-### 一、官方查询渠道
-
-为保障个人信息安全并获取准确成绩,考生应优先选择以下官方渠道进行查询:
-
-1.  **中国教育考试网(中小学教师资格考试网站)**
-    这是最权威、最直接的官方渠道,也是查询教资笔试和面试成绩的首要官方平台。
-    *   **官方网址**:`https://ntce.neea.edu.cn`
-    *   **成绩查询入口**:通常在官网首页设有明显的“成绩查询”入口,也可直接访问成绩查询页面:`https://cjcx.neea.edu.cn`。
-    *   **查询步骤**:访问官网后,找到“考生服务”或“成绩查询”入口,输入您的姓名、证件号码等信息即可查询。
-
-2.  **中国教育考试网微信小程序**
-    为方便考生,教育部教育考试院也开通了官方微信小程序。
-    *   **使用方法**:在微信APP中搜索“**中国教育考试网**”小程序,进入后即可查询成绩。
-
-**特别说明:**
-*   **中国教师资格网**(`https://www.jszg.edu.cn`)主要负责教师资格的认定工作。虽然有时会转发成绩查询通知,但查询具体分数仍需跳转到“中国教育考试网”。
-
-### 二、成绩发布时间
-
-教资考试分为笔试和面试,每年举行两次(上半年和下半年)。成绩发布时间通常在考试结束后的1-2个月内。
-
-*   **笔试成绩**:一般在考后约一个月左右公布。
-    *   **上半年**:通常在4月中下旬。
-    *   **下半年**:通常在11月上旬。
-    *   **例如**:2025年下半年的笔试成绩已于2025年11月7日上午10点发布。
-
-*   **面试成绩**:一般在考后约一个月左右公布。
-    *   **上半年**:通常在6月中旬。
-    *   **下半年**:通常在次年1月上旬。
-    *   **例如**:2025年上半年的面试成绩已于2025年6月11日上午10点发布。
-
-### 三、官方通知方式
-
-教育部教育考试院会通过以下官方渠道发布成绩查询通知:
-
-1.  **官方网站**:最权威的成绩发布通知会发布在**中小学教师资格考试网**(`https://ntce.neea.edu.cn`)的“通知公告”栏目。
-2.  **官方微信**:“中国教育考试网”微信小程序或相关官方微信公众号也会同步推送通知。
-
-### 四、重要提示
-
-*   **合格标准**:笔试各科目的报告满分均为120分,**70分及以上为合格**。
-*   **成绩复核**:如果考生对本人的笔试成绩有异议,可以在规定时间内申请成绩复核。具体复核流程和时间请关注所在省份教育考试院的官方通知。
-*   **查询安全**:请务必通过上述官方渠道进行成绩查询,避免使用非官方的第三方网站,以防个人信息泄露或获取错误信息。
-*   **建议收藏**:建议考生收藏“中国教育考试网”的中小学教师资格考试页面(`https://ntce.neea.edu.cn`),以便及时获取最权威和准确的信息。

+ 0 - 14
knowledge_v2/.cache/9f510b2a8348/llm_search/search_results/search_result_001.txt

@@ -1,14 +0,0 @@
-查询中小学教师资格考试(教资)成绩的官方信息发布渠道主要有以下两个:
-
-### 1. 中国教育考试网 (官方网站)
-这是最权威、最直接的官方渠道。
-*   **官网地址**: `https://ntce.neea.edu.cn`
-*   **成绩查询入口**: 通常在官网首页会有明显的“成绩查询”入口,也可以直接访问成绩查询页面:`https://cjcx.neea.edu.cn`
-
-### 2. 中国教育考试网微信小程序
-为方便考生,教育部教育考试院也开通了官方微信小程序。
-*   **使用方法**: 在微信APP中搜索“**中国教育考试网**”小程序,进入后即可查询成绩。
-
-**重要提示**:
-*   成绩发布时,官方会通过 **中国教育考试网** 发布正式的《关于[年份][上下半年]中小学教师资格考试(笔试/面试)成绩发布的通知》。所有考生应以此通知为准。
-*   建议优先使用上述两个官方渠道进行查询,以确保信息的准确性和安全性,避免通过非官方的第三方网站查询,以防个人信息泄露或获取错误信息。

+ 0 - 24
knowledge_v2/.cache/9f510b2a8348/llm_search/search_results/search_result_002.txt

@@ -1,24 +0,0 @@
-中小学教师资格考试(教资考试)的成绩查询日期和通知方式如下:
-
-### 一、成绩查询日期
-教资考试分为笔试和面试,每年举行两次(上半年和下半年),其成绩发布时间是固定的,通常在考试结束后的1-2个月内。
-
-*   **笔试成绩**:一般在考后约一个月左右公布。
-    *   **上半年**:通常在4月中下旬。
-    *   **下半年**:通常在11月上旬。
-    *   **例如**:2025年下半年的笔试成绩已于**2025年11月7日**上午10点发布。
-
-*   **面试成绩**:一般在考后约一个月左右公布。
-    *   **上半年**:通常在6月中旬。
-    *   **下半年**:通常在次年1月上旬。
-    *   **例如**:2025年上半年的面试成绩已于**2025年6月11日**上午10点发布。
-
-### 二、官方通知方式
-教育部教育考试院会通过以下官方渠道发布成绩查询通知:
-1.  **官方网站**:最权威的通知会发布在 **[中小学教师资格考试网](https://ntce.neea.edu.cn/)** 的“通知公告”栏目。
-2.  **官方微信**:“中国教育考试网”微信小程序或相关官方微信公众号也会同步推送通知。
-
-### 三、成绩查询方式
-成绩开放后,考生可以通过以下方式查询:
-1.  **网站查询**:登录 **[中小学教师资格考试网](https://ntce.neea.edu.cn/ntce/)** 进行查询。
-2.  **微信小程序**:在微信中搜索并使用“**中国教育考试网**”小程序进行查询。

+ 0 - 14
knowledge_v2/.cache/9f510b2a8348/llm_search/search_results/search_result_003.txt

@@ -1,14 +0,0 @@
-根据教育部教育考试院(原教育部考试中心)的最新公告,关于2025年下半年中小学教师资格考试(笔试)的成绩查询信息如下:
-
-### 成绩发布时间
-**2025年11月7日上午10时**
-
-### 成绩查询方式
-1.  **官方网站**:考生可登录“中小学教师资格考试”网站([http://ntce.neea.edu.cn](http://ntce.neea.edu.cn))进行查询。
-2.  **中国教育考试网**:也可通过中国教育考试网([https://cjcx.neea.edu.cn](https://cjcx.neea.edu.cn))进行成绩查询。
-
-### 重要说明
-*   笔试各科目的报告满分均为120分,**70分及以上为合格**。
-*   如果考生对本人的笔试成绩有异议,可以在规定时间内申请成绩复核。具体复核流程和时间请关注所在省份教育考试院的官方通知。
-
-您可以直接访问上述官方网站查询您的成绩。

+ 0 - 17
knowledge_v2/.cache/9f510b2a8348/llm_search/search_results/search_result_004.txt

@@ -1,17 +0,0 @@
-教师资格证考试(教资)的成绩查询信息主要在以下两个官方网站公布:
-
-1.  **中国教育考试网 - 中小学教师资格考试 (NTCE) 项目**
-    *   **网址**:`https://ntce.neea.edu.cn`
-    *   **说明**:这是教育部考试中心主办的官方网站,是查询教资笔试和面试成绩的**首要官方渠道**。成绩发布后,该网站会开通专门的成绩查询入口,并发布相关的通知公告。
-
-2.  **中国教师资格网**
-    *   **网址**:`https://www.jszg.edu.cn`
-    *   **说明**:这个网站主要负责教师资格的**认定**工作。虽然有时也会转发成绩查询的通知,但**查询具体分数仍需跳转到“中国教育考试网”**。在拿到考试合格证明后,后续的认定申请流程都在此网站进行。
-
-**总结一下查询步骤:**
-
-*   成绩公布时,请直接访问 **中国教育考试网** (`https://ntce.neea.edu.cn`)。
-*   在网站首页找到“**考生服务**”或“**成绩查询**”的入口。
-*   输入您的姓名、证件号码等信息即可查询。
-
-建议您收藏“中国教育考试网”的中小学教师资格考试页面,以获取最权威和及时的信息。

+ 0 - 26
knowledge_v2/.cache/9f510b2a8348/multi_search/final_knowledge.txt

@@ -1,26 +0,0 @@
-教资查分的信息主要通过官方渠道发布和查询,以确保准确性和安全性。针对“2025年11月7日”这个时间点,正是下半年笔试成绩发布的典型时期。
-
-以下是关于教资查分信息来源的详细说明:
-
-### 一、成绩发布信息来源与查询渠道
-
-1.  **官方发布与通知**
-    *   **中小学教师资格考试网(中国教育考试网)**:这是最权威的官方网站,网址为`https://ntce.neea.edu.cn`。成绩发布通知会第一时间在此网站的“通知公告”栏目公布。
-    *   **中国教育考试网微信小程序/官方微信公众号**:教育部教育考试院会通过其官方微信小程序或相关官方微信公众号同步推送成绩查询通知。
-    *   **中国教师资格网**:`https://www.jszg.edu.cn` 虽然主要负责认定工作,有时也会转发成绩查询通知,但查询具体分数仍需跳转至“中国教育考试网”。
-
-2.  **成绩查询入口**
-    *   **中国教育考试网**:考生可通过访问`https://ntce.neea.edu.cn`官网首页的“成绩查询”入口,或直接访问成绩查询页面`https://cjcx.neea.edu.cn`进行查询。
-    *   **中国教育考试网微信小程序**:在微信APP中搜索并进入“中国教育考试网”小程序,即可查询成绩。
-
-### 二、成绩发布时间与“2025年11月7日”的关联
-
-教资笔试成绩通常在考试结束后的一个月左右公布。
-
-*   **下半年笔试成绩**:一般在每年的11月上旬发布。
-*   **“2025年11月7日”的具体情况**:根据官方发布惯例及已有信息,2025年下半年的笔试成绩已于**2025年11月7日上午10点**发布。因此,您提到的“2025年11月7日”正是下半年教资笔试成绩的官方公布时间。
-
-### 三、重要提示
-
-*   为保障个人信息安全并获取准确成绩,务必通过上述官方渠道进行成绩查询,避免使用非官方的第三方网站。
-*   建议考生收藏“中国教育考试网”的中小学教师资格考试页面(`https://ntce.neea.edu.cn`),以便及时获取最权威和准确的信息。

+ 0 - 26
knowledge_v2/.cache/9f510b2a8348/multi_search/merged_knowledge.txt

@@ -1,26 +0,0 @@
-教资查分的信息主要通过官方渠道发布和查询,以确保准确性和安全性。针对“2025年11月7日”这个时间点,正是下半年笔试成绩发布的典型时期。
-
-以下是关于教资查分信息来源的详细说明:
-
-### 一、成绩发布信息来源与查询渠道
-
-1.  **官方发布与通知**
-    *   **中小学教师资格考试网(中国教育考试网)**:这是最权威的官方网站,网址为`https://ntce.neea.edu.cn`。成绩发布通知会第一时间在此网站的“通知公告”栏目公布。
-    *   **中国教育考试网微信小程序/官方微信公众号**:教育部教育考试院会通过其官方微信小程序或相关官方微信公众号同步推送成绩查询通知。
-    *   **中国教师资格网**:`https://www.jszg.edu.cn` 虽然主要负责认定工作,有时也会转发成绩查询通知,但查询具体分数仍需跳转至“中国教育考试网”。
-
-2.  **成绩查询入口**
-    *   **中国教育考试网**:考生可通过访问`https://ntce.neea.edu.cn`官网首页的“成绩查询”入口,或直接访问成绩查询页面`https://cjcx.neea.edu.cn`进行查询。
-    *   **中国教育考试网微信小程序**:在微信APP中搜索并进入“中国教育考试网”小程序,即可查询成绩。
-
-### 二、成绩发布时间与“2025年11月7日”的关联
-
-教资笔试成绩通常在考试结束后的一个月左右公布。
-
-*   **下半年笔试成绩**:一般在每年的11月上旬发布。
-*   **“2025年11月7日”的具体情况**:根据官方发布惯例及已有信息,2025年下半年的笔试成绩已于**2025年11月7日上午10点**发布。因此,您提到的“2025年11月7日”正是下半年教资笔试成绩的官方公布时间。
-
-### 三、重要提示
-
-*   为保障个人信息安全并获取准确成绩,务必通过上述官方渠道进行成绩查询,避免使用非官方的第三方网站。
-*   建议考生收藏“中国教育考试网”的中小学教师资格考试页面(`https://ntce.neea.edu.cn`),以便及时获取最权威和准确的信息。

+ 0 - 115
knowledge_v2/clear_cache.py

@@ -1,115 +0,0 @@
-"""
-清除缓存工具
-"""
-
-import os
-import sys
-import shutil
-
-# 添加路径
-current_dir = os.path.dirname(os.path.abspath(__file__))
-root_dir = os.path.dirname(current_dir)
-sys.path.insert(0, root_dir)
-
-from knowledge_v2.cache_manager import CacheManager
-
-def clear_all_cache():
-    """清除所有缓存"""
-    cache = CacheManager()
-    
-    if os.path.exists(cache.base_cache_dir):
-        print(f"缓存目录: {cache.base_cache_dir}")
-        
-        # 统计信息
-        total_size = 0
-        file_count = 0
-        for root, dirs, files in os.walk(cache.base_cache_dir):
-            for file in files:
-                file_path = os.path.join(root, file)
-                total_size += os.path.getsize(file_path)
-                file_count += 1
-        
-        print(f"文件数量: {file_count}")
-        print(f"总大小: {total_size / 1024:.2f} KB")
-        
-        # 确认删除
-        response = input("\n确认清除所有缓存?(yes/no): ")
-        if response.lower() == 'yes':
-            shutil.rmtree(cache.base_cache_dir)
-            os.makedirs(cache.base_cache_dir)
-            print("✓ 已清除所有缓存")
-        else:
-            print("取消操作")
-    else:
-        print("缓存目录不存在")
-
-def clear_question_cache(question: str):
-    """清除特定问题的缓存"""
-    cache = CacheManager()
-    
-    import hashlib
-    question_hash = hashlib.md5(question.encode('utf-8')).hexdigest()[:12]
-    question_dir = os.path.join(cache.base_cache_dir, question_hash)
-    
-    if os.path.exists(question_dir):
-        # 显示问题文本
-        question_file = os.path.join(question_dir, 'question.txt')
-        if os.path.exists(question_file):
-            with open(question_file, 'r', encoding='utf-8') as f:
-                cached_question = f.read()
-            print(f"找到缓存的问题: {cached_question}")
-        
-        # 确认删除
-        response = input("\n确认清除此问题的缓存?(yes/no): ")
-        if response.lower() == 'yes':
-            shutil.rmtree(question_dir)
-            print("✓ 已清除该问题的缓存")
-        else:
-            print("取消操作")
-    else:
-        print("未找到该问题的缓存")
-
-def list_cached_questions():
-    """列出所有缓存的问题"""
-    cache = CacheManager()
-    
-    if not os.path.exists(cache.base_cache_dir):
-        print("缓存目录不存在")
-        return
-    
-    print("缓存的问题列表:")
-    print("=" * 60)
-    
-    count = 0
-    for dir_name in os.listdir(cache.base_cache_dir):
-        question_dir = os.path.join(cache.base_cache_dir, dir_name)
-        if os.path.isdir(question_dir):
-            question_file = os.path.join(question_dir, 'question.txt')
-            if os.path.exists(question_file):
-                with open(question_file, 'r', encoding='utf-8') as f:
-                    question = f.read()
-                count += 1
-                print(f"{count}. [{dir_name}] {question[:50]}...")
-    
-    print("=" * 60)
-    print(f"总计: {count} 个缓存问题")
-
-if __name__ == "__main__":
-    print("缓存管理工具")
-    print("=" * 60)
-    print("1. 列出所有缓存")
-    print("2. 清除所有缓存")
-    print("3. 清除特定问题的缓存")
-    print("=" * 60)
-    
-    choice = input("请选择操作 (1/2/3): ")
-    
-    if choice == "1":
-        list_cached_questions()
-    elif choice == "2":
-        clear_all_cache()
-    elif choice == "3":
-        question = input("请输入问题的完整文本(需要与原问题完全一致): ")
-        clear_question_cache(question)
-    else:
-        print("无效的选择")

+ 259 - 0
knowledge_v2/execution_collector.py

@@ -0,0 +1,259 @@
+"""
+执行记录收集器
+
+从各个模块的缓存目录收集执行详情,汇总成完整的execution_record.json
+"""
+
+import os
+import json
+import hashlib
+from typing import Dict, Any, List
+from loguru import logger
+
+
+class ExecutionCollector:
+    """执行记录收集器"""
+    
+    def __init__(self, base_cache_dir: str = None):
+        """
+        初始化
+        
+        Args:
+            base_cache_dir: 缓存基础目录,默认为当前目录下的.cache
+        """
+        if base_cache_dir is None:
+            current_dir = os.path.dirname(os.path.abspath(__file__))
+            base_cache_dir = os.path.join(current_dir, '.cache')
+        
+        self.base_cache_dir = base_cache_dir
+    
+    def collect_execution_record(self, cache_key: str, input_info: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        收集完整的执行记录
+        
+        Args:
+            cache_key: 缓存键(通常是combined_question)
+            input_info: 输入信息 {"question": ..., "post_info": ..., "persona_info": ...}
+            
+        Returns:
+            dict: 完整的执行记录
+        """
+        logger.info("=" * 60)
+        logger.info("开始收集执行记录...")
+        
+        # 计算hash
+        question_hash = hashlib.md5(cache_key.encode('utf-8')).hexdigest()[:12]
+        cache_dir = os.path.join(self.base_cache_dir, question_hash)
+        
+        if not os.path.exists(cache_dir):
+            logger.warning(f"缓存目录不存在: {cache_dir}")
+            return self._create_empty_record(input_info)
+        
+        # 初始化执行记录
+        execution_record = {
+            "input": input_info,
+            "execution": {
+                "modules": {}
+            },
+            "result": {
+                "type": None,
+                "content": None,
+                "raw_data": None
+            },
+            "metadata": {
+                "execution_time": 0,
+                "cache_hits": [],
+                "errors": []
+            }
+        }
+        
+        # 收集各模块的执行详情
+        try:
+            # 1. 收集 function_knowledge 的详情
+            function_detail = self._collect_function_knowledge_detail(cache_dir)
+            if function_detail:
+                execution_record["execution"]["modules"]["function_knowledge"] = function_detail
+            
+            # 2. 收集 multi_search 的详情
+            multi_detail = self._collect_multi_search_detail(cache_dir)
+            if multi_detail:
+                execution_record["execution"]["modules"]["multi_search"] = multi_detail
+            
+            # 3. 收集 llm_search 的详情
+            llm_detail = self._collect_llm_search_detail(cache_dir)
+            if llm_detail:
+                execution_record["execution"]["modules"]["llm_search"] = llm_detail
+
+            # 4.设置结果信息
+            result_detail = self._collect_result_detail(cache_dir)
+            if result_detail:
+                execution_record["result"] = result_detail
+            
+            # 5. 计算总结信息
+            self._calculate_summary(execution_record)
+            
+            logger.info("✓ 执行记录收集完成")
+            logger.info("=" * 60)
+            
+        except Exception as e:
+            logger.error(f"✗ 收集执行记录失败: {e}")
+            execution_record["metadata"]["errors"].append(str(e))
+        
+        return execution_record
+    
+    def _collect_function_knowledge_detail(self, cache_dir: str) -> Dict[str, Any]:
+        """收集function_knowledge模块的详情"""
+        detail_file = os.path.join(cache_dir, 'function_knowledge', 'execution_detail.json')
+        
+        if os.path.exists(detail_file):
+            try:
+                with open(detail_file, 'r', encoding='utf-8') as f:
+                    detail = json.load(f)
+                logger.info("  ✓ 收集 function_knowledge 详情")
+                return detail
+            except Exception as e:
+                logger.error(f"  ✗ 读取 function_knowledge 详情失败: {e}")
+        
+        return None
+    
+    def _collect_multi_search_detail(self, cache_dir: str) -> Dict[str, Any]:
+        """收集multi_search模块的详情"""
+        detail_file = os.path.join(cache_dir, 'multi_search', 'execution_detail.json')
+        
+        if os.path.exists(detail_file):
+            try:
+                with open(detail_file, 'r', encoding='utf-8') as f:
+                    detail = json.load(f)
+                logger.info("  ✓ 收集 multi_search 详情")
+                return detail
+            except Exception as e:
+                logger.error(f"  ✗ 读取 multi_search 详情失败: {e}")
+        
+        return None
+    
+    def _collect_llm_search_detail(self, cache_dir: str) -> Dict[str, Any]:
+        """收集llm_search模块的详情"""
+        detail_file = os.path.join(cache_dir, 'llm_search', 'execution_detail.json')
+        
+        if os.path.exists(detail_file):
+            try:
+                with open(detail_file, 'r', encoding='utf-8') as f:
+                    detail = json.load(f)
+                logger.info("  ✓ 收集 llm_search 详情")
+                return detail
+            except Exception as e:
+                logger.error(f"  ✗ 读取 llm_search 详情失败: {e}")
+        
+        return None
+
+    def _collect_result_detail(self, cache_dir: str) -> Dict[str, Any]:
+        """收集result模块的详情"""
+        detail_file = os.path.join(cache_dir, 'function_knowledge', 'tool_result.json')
+
+        if os.path.exists(detail_file):
+            try:
+                with open(detail_file, 'r', encoding='utf-8') as f:
+                    detail = json.load(f)
+                logger.info("  ✓ 收集 result 详情")
+                return detail
+            except Exception as e:
+                logger.error(f"  ✗ 读取 result 详情失败: {e}")
+
+        return None
+    
+    def _calculate_summary(self, execution_record: Dict[str, Any]):
+        """计算总结信息"""
+        total_time = 0
+        cache_hits = []
+        
+        # 遍历所有模块
+        for module_name, module_detail in execution_record["execution"]["modules"].items():
+            if "execution_time" in module_detail:
+                total_time += module_detail["execution_time"]
+            
+            if "cache_hits" in module_detail:
+                cache_hits.extend([f"{module_name}/{hit}" for hit in module_detail["cache_hits"]])
+        
+        execution_record["metadata"]["execution_time"] = total_time
+        execution_record["metadata"]["cache_hits"] = cache_hits
+    
+    def _create_empty_record(self, input_info: Dict[str, Any]) -> Dict[str, Any]:
+        """创建空的执行记录"""
+        return {
+            "input": input_info,
+            "execution": {
+                "steps": [],
+                "modules": {}
+            },
+            "result": {
+                "type": "error",
+                "content": "缓存目录不存在",
+                "raw_data": None
+            },
+            "metadata": {
+                "execution_time": 0,
+                "cache_hits": [],
+                "errors": ["缓存目录不存在"]
+            }
+        }
+    
+    def save_execution_record(self, cache_key: str, execution_record: Dict[str, Any]) -> str:
+        """
+        保存执行记录到文件
+        
+        Args:
+            cache_key: 缓存键
+            execution_record: 执行记录
+            
+        Returns:
+            str: 保存的文件路径
+        """
+        question_hash = hashlib.md5(cache_key.encode('utf-8')).hexdigest()[:12]
+        cache_dir = os.path.join(self.base_cache_dir, question_hash)
+        os.makedirs(cache_dir, exist_ok=True)
+        
+        output_file = os.path.join(cache_dir, 'execution_record.json')
+        
+        try:
+            with open(output_file, 'w', encoding='utf-8') as f:
+                json.dump(execution_record, f, ensure_ascii=False, indent=2)
+            
+            logger.info(f"✓ 执行记录已保存: {output_file}")
+            return output_file
+            
+        except Exception as e:
+            logger.error(f"✗ 保存执行记录失败: {e}")
+            raise
+
+
+def collect_and_save_execution_record(cache_key: str, input_info: Dict[str, Any]) -> Dict[str, Any]:
+    """
+    便捷函数:收集并保存执行记录
+    
+    Args:
+        cache_key: 缓存键
+        input_info: 输入信息
+        
+    Returns:
+        dict: 完整的执行记录
+    """
+    collector = ExecutionCollector()
+    execution_record = collector.collect_execution_record(cache_key, input_info)
+    collector.save_execution_record(cache_key, execution_record)
+    return execution_record
+
+
+if __name__ == "__main__":
+    # 测试
+    import time
+    
+    cache_key = "测试问题||无||测试人设"
+    input_info = {
+        "question": "测试问题",
+        "post_info": "无",
+        "persona_info": "测试人设",
+        "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
+    }
+    
+    record = collect_and_save_execution_record(cache_key, input_info)
+    print(json.dumps(record, ensure_ascii=False, indent=2))

+ 183 - 230
knowledge_v2/function_knowledge.py

@@ -41,9 +41,72 @@ class FunctionKnowledge:
         self.prompt_dir = os.path.join(current_dir, "prompt")
         self.use_cache = use_cache
         self.cache = CacheManager() if use_cache else None
+        
+        # 执行详情收集
+        self.execution_detail = {
+            "generate_query": {},
+            "select_tool": {},
+            "extract_params": {},
+            "execution_time": 0,
+            "cache_hits": []
+        }
+        
         logger.info(f"缓存状态: {'启用' if use_cache else '禁用'}")
         logger.info("=" * 80)
         
+    def _save_execution_detail(self, cache_key: str):
+        """保存执行详情到缓存(支持合并旧记录)"""
+        if not self.use_cache or not self.cache:
+            return
+        
+        try:
+            import hashlib
+            question_hash = hashlib.md5(cache_key.encode('utf-8')).hexdigest()[:12]
+            detail_dir = os.path.join(
+                self.cache.base_cache_dir,
+                question_hash,
+                'function_knowledge'
+            )
+            os.makedirs(detail_dir, exist_ok=True)
+            
+            detail_file = os.path.join(detail_dir, 'execution_detail.json')
+            
+            # 准备最终要保存的数据,默认为当前内存中的数据
+            final_detail = self.execution_detail.copy()
+            
+            # 尝试读取旧文件进行合并
+            if os.path.exists(detail_file):
+                try:
+                    with open(detail_file, 'r', encoding='utf-8') as f:
+                        old_detail = json.load(f)
+                    
+                    # 智能合并逻辑:保留更有价值的历史信息
+                    for key, new_val in self.execution_detail.items():
+                        # 跳过非字典字段或旧文件中不存在的字段
+                        if not isinstance(new_val, dict) or key not in old_detail:
+                            continue
+                            
+                        old_val = old_detail[key]
+                        if not isinstance(old_val, dict):
+                            continue
+                            
+                        # 核心逻辑:如果新记录是缓存命中(cached=True),而旧记录包含prompt(说明是当初生成的)
+                        # 则保留旧记录,防止被简略信息覆盖
+                        if new_val.get("cached", False) is True and "prompt" in old_val:
+                            # logger.debug(f"  保留 {key} 的历史详细记录")
+                            final_detail[key] = old_val
+                            
+                except Exception as e:
+                    logger.warning(f"  ⚠ 读取旧详情失败,将使用新记录: {e}")
+
+            with open(detail_file, 'w', encoding='utf-8') as f:
+                json.dump(final_detail, f, ensure_ascii=False, indent=2)
+            
+            logger.info(f"✓ 执行详情已保存: {detail_file}")
+            
+        except Exception as e:
+            logger.error(f"✗ 保存执行详情失败: {e}")
+
     def _load_prompt(self, filename: str) -> str:
         """加载prompt文件内容"""
         prompt_path = os.path.join(self.prompt_dir, filename)
@@ -52,108 +115,112 @@ class FunctionKnowledge:
         with open(prompt_path, 'r', encoding='utf-8') as f:
             return f.read().strip()
 
-    def generate_query(self, question: str, post_info: str, persona_info: str) -> tuple:
+    def generate_query(self, question: str, post_info: str, persona_info: str) -> str:
         """
         生成查询语句
         
         Returns:
-            tuple: (query, detail_info)
-            - query: 生成的查询语句
-            - detail_info: 详细信息dict,包含prompt和response
+            str: 生成的查询语句
         """
         logger.info(f"[步骤1] 生成Query...")
         
         # 组合问题的唯一标识
         combined_question = f"{question}||{post_info}||{persona_info}"
         
-        detail_info = {"cached": False, "prompt": None, "response": None}
-        
         # 尝试从缓存读取
         if self.use_cache:
             cached_query = self.cache.get(combined_question, 'function_knowledge', 'generated_query.txt')
             if cached_query:
                 logger.info(f"✓ 使用缓存的Query: {cached_query}")
-                detail_info["cached"] = True
-                return cached_query, detail_info
+                # 记录缓存命中
+                self.execution_detail["generate_query"].update({"cached": True, "query": cached_query})
+                return cached_query
         
         try:
             prompt_template = self._load_prompt("function_generate_query_prompt.md")
             prompt = prompt_template.replace("{question}", question)
             
-            detail_info["prompt"] = prompt
-            
             logger.info("→ 调用Gemini生成Query...")
             query = generate_text(prompt=prompt)
             query = query.strip()
             
-            detail_info["response"] = query
-            
             logger.info(f"✓ 生成Query: {query}")
             
             # 写入缓存
             if self.use_cache:
                 self.cache.set(combined_question, 'function_knowledge', 'generated_query.txt', query)
             
-            return query, detail_info
+            # 记录详情
+            self.execution_detail["generate_query"] = {
+                "cached": False,
+                "prompt": prompt,
+                "response": query,
+                "query": query
+            }
+            
+            return query
         except Exception as e:
             logger.error(f"✗ 生成Query失败: {e}")
-            detail_info["error"] = str(e)
-            return question, detail_info  # 降级使用原问题
+            return question  # 降级使用原问题
 
-    def select_tool(self, combined_question: str, query: str) -> tuple:
+    def select_tool(self, combined_question: str, query: str) -> str:
         """
         选择合适的工具
         
         Returns:
-            tuple: (tool_name, detail_info)
+            str: 工具名称,如果没有合适的工具则返回"None"
         """
         logger.info(f"[步骤2] 选择工具...")
         
-        detail_info = {"cached": False, "prompt": None, "response": None, "available_tools_count": 0}
-        
         # 尝试从缓存读取
         if self.use_cache:
             cached_tool = self.cache.get(combined_question, 'function_knowledge', 'selected_tool.txt')
             if cached_tool:
                 logger.info(f"✓ 使用缓存的工具: {cached_tool}")
-                detail_info["cached"] = True
-                return cached_tool, detail_info
+                # 记录缓存命中
+                self.execution_detail["select_tool"].update({
+                    "cached": True,
+                    "tool_name": cached_tool
+                })
+                return cached_tool
         
         try:
             all_tool_infos = self._load_prompt("all_tools_infos.md")
             if not all_tool_infos:
                 logger.info("  工具库为空,无可用工具")
-                return "None", detail_info
+                return "None"
             
             tool_count = len(all_tool_infos.split('--- Tool:')) - 1
-            detail_info["available_tools_count"] = tool_count
             logger.info(f"  当前可用工具数: {tool_count}")
                 
             prompt_template = self._load_prompt("function_knowledge_select_tools_prompt.md")
             prompt = prompt_template.replace("{all_tool_infos}", all_tool_infos)
             
-            detail_info["prompt"] = prompt
-            detail_info["tool_infos"] = all_tool_infos
-            
             logger.info("→ 调用Gemini选择工具...")
             tool_name = generate_text(prompt=prompt)
             tool_name = tool_name.strip()
             
-            detail_info["response"] = tool_name
-            
             logger.info(f"✓ 选择结果: {tool_name}")
             
             # 写入缓存
             if self.use_cache:
                 self.cache.set(combined_question, 'function_knowledge', 'selected_tool.txt', tool_name)
             
-            return tool_name, detail_info
+            # 记录详情
+            self.execution_detail["select_tool"] = {
+                "cached": False,
+                "prompt": prompt,
+                "response": tool_name,
+                "tool_name": tool_name,
+                "available_tools_count": tool_count
+            }
+            
+            return tool_name
         except Exception as e:
             logger.error(f"✗ 选择工具失败: {e}")
-            detail_info["error"] = str(e)
-            return "None", detail_info
+            return "None"
 
-    def extract_tool_params(self, combined_question: str, tool_name: str, query: str) -> tuple:
+    def extract_tool_params(self, combined_question: str, tool_name: str, query: str) -> dict:
         """
         根据工具信息和查询提取调用参数
         
@@ -163,32 +230,29 @@ class FunctionKnowledge:
             query: 查询内容
             
         Returns:
-            tuple: (params, detail_info)
+            dict: 提取的参数字典
         """
         logger.info(f"[步骤3] 提取工具参数...")
         
-        # 初始化detail_info
-        detail_info = {"cached": False, "prompt": None, "response": None, "tool_info": None}
-        
         # 尝试从缓存读取
         if self.use_cache:
             cached_params = self.cache.get(combined_question, 'function_knowledge', 'tool_params.json')
             if cached_params:
                 logger.info(f"✓ 使用缓存的参数: {cached_params}")
-                detail_info["cached"] = True
-                return cached_params, detail_info
+                # 记录缓存命中
+                self.execution_detail["extract_params"].update({
+                    "cached": True,
+                    "params": cached_params
+                })
+                return cached_params
         
         try:
             # 获取工具信息
             tool_info = get_tool_info(tool_name)
             if not tool_info:
                 logger.warning(f"  ⚠ 未找到工具 {tool_name} 的信息,使用默认参数")
-                # 降级:使用query作为keyword
-                default_params = {"keyword": query}
-                detail_info["fallback"] = "tool_info_not_found"
-                return default_params, detail_info
+                return {"keyword": query}
             
-            detail_info["tool_info"] = tool_info
             logger.info(f"  工具 {tool_name} 信息长度: {len(tool_info)}")
             
             # 加载prompt
@@ -198,12 +262,9 @@ class FunctionKnowledge:
                 tool_info=tool_info
             )
             
-            detail_info["prompt"] = prompt
-            
             # 调用LLM提取参数
             logger.info("  → 调用Gemini提取参数...")
             response_text = generate_text(prompt=prompt)
-            detail_info["response"] = response_text
             
             # 解析JSON
             logger.info("  → 解析参数JSON...")
@@ -226,7 +287,15 @@ class FunctionKnowledge:
                 if self.use_cache:
                     self.cache.set(combined_question, 'function_knowledge', 'tool_params.json', params)
                 
-                return params, detail_info
+                # 记录详情
+                self.execution_detail["extract_params"].update({
+                    "cached": False,
+                    "prompt": prompt,
+                    "response": response_text,
+                    "params": params
+                })
+                
+                return params
                 
             except json.JSONDecodeError as e:
                 logger.error(f"  ✗ 解析JSON失败: {e}")
@@ -234,16 +303,12 @@ class FunctionKnowledge:
                 # 降级:使用query作为keyword
                 default_params = {"keyword": query}
                 logger.warning(f"  使用默认参数: {default_params}")
-                detail_info["fallback"] = "json_decode_error"
-                return default_params, detail_info
+                return default_params
                 
         except Exception as e:
             logger.error(f"✗ 提取工具参数失败: {e}")
             # 降级:使用query作为keyword
-            default_params = {"keyword": query}
-            detail_info["error"] = str(e)
-            detail_info["fallback"] = "exception"
-            return default_params, detail_info
+            return {"keyword": query}
 
     def save_knowledge_to_file(self, knowledge: str, combined_question: str):
         """保存获取到的知识到文件"""
@@ -275,20 +340,14 @@ class FunctionKnowledge:
 
     def get_knowledge(self, question: str, post_info: str, persona_info: str) -> dict:
         """
-        获取方法知识的主流程
+        获取方法知识的主流程(重构后)
         
         Returns:
-            dict: 包含完整执行信息的字典
-            {
-                "input": {...},          # 原始输入
-                "execution": {...},      # 执行过程信息
-                "result": {...},         # 最终结果
-                "metadata": {...}        # 元数据
-            }
+            dict: 完整的执行记录
         """
         import time
-        start_time = time.time()
         timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
+        start_time = time.time()
         
         logger.info("=" * 80)
         logger.info(f"Function Knowledge - 开始处理")
@@ -300,86 +359,17 @@ class FunctionKnowledge:
         # 组合问题的唯一标识
         combined_question = f"{question}||{post_info}||{persona_info}"
         
-        # 初始化执行记录
-        execution_record = {
-            "input": {
-                "question": question,
-                "post_info": post_info,
-                "persona_info": persona_info,
-                "timestamp": timestamp
-            },
-            "execution": {
-                "steps": [],
-                "tool_info": None,
-                "knowledge_search_info": None
-            },
-            "result": {
-                "type": None,  # "tool" 或 "knowledge"
-                "content": None,
-                "raw_data": None
-            },
-            "metadata": {
-                "execution_time": None,
-                "cache_hits": [],
-                "errors": []
-            }
-        }
-        
-        # 检查最终结果缓存
-        if self.use_cache:
-            cached_final = self.cache.get(combined_question, 'function_knowledge', 'final_result.json')
-            if cached_final:
-                logger.info(f"✓ 使用缓存的最终结果")
-                logger.info("=" * 80 + "\n")
-                # 如果是完整的执行记录,直接返回
-                if isinstance(cached_final, dict) and "execution" in cached_final:
-                    return cached_final
-                # 否则构造一个简单的返回
-                return {
-                    "input": execution_record["input"],
-                    "execution": {"cached": True},
-                    "result": {"type": "cached", "content": cached_final},
-                    "metadata": {"cache_hit": True}
-                }
-        
         try:
             # 步骤1: 生成Query
-            step1_start = time.time()
-            query, query_detail = self.generate_query(question, post_info, persona_info)
-            execution_record["execution"]["steps"].append({
-                "step": 1,
-                "name": "generate_query",
-                "duration": time.time() - step1_start,
-                "output": query,
-                "detail": query_detail  # 包含prompt和response
-            })
+            query = self.generate_query(question, post_info, persona_info)
             
             # 步骤2: 选择工具
-            step2_start = time.time()
-            tool_name, tool_select_detail = self.select_tool(combined_question, query)
-            execution_record["execution"]["steps"].append({
-                "step": 2,
-                "name": "select_tool",
-                "duration": time.time() - step2_start,
-                "output": tool_name,
-                "detail": tool_select_detail  # 包含prompt、response和可用工具列表
-            })
+            tool_name = self.select_tool(combined_question, query)
             
-            result_content = None
             if tool_name and tool_name != "None":
                 # 路径A: 使用工具
-                execution_record["result"]["type"] = "tool"
-                
                 # 步骤3: 提取参数
-                step3_start = time.time()
-                arguments, params_detail = self.extract_tool_params(combined_question, tool_name, query)
-                execution_record["execution"]["steps"].append({
-                    "step": 3,
-                    "name": "extract_tool_params",
-                    "duration": time.time() - step3_start,
-                    "output": arguments,
-                    "detail": params_detail  # 包含prompt、response和工具信息
-                })
+                arguments = self.extract_tool_params(combined_question, tool_name, query)
                 
                 # 步骤4: 调用工具
                 logger.info(f"[步骤4] 调用工具: {tool_name}")
@@ -389,102 +379,52 @@ class FunctionKnowledge:
                     cached_tool_result = self.cache.get(combined_question, 'function_knowledge', 'tool_result.json')
                     if cached_tool_result:
                         logger.info(f"✓ 使用缓存的工具调用结果")
-                        execution_record["metadata"]["cache_hits"].append("tool_result")
                         tool_result = cached_tool_result
                     else:
-                        step4_start = time.time()
                         logger.info(f"  → 调用工具,参数: {arguments}")
                         tool_result = call_tool(tool_name, arguments)
-                        
                         # 缓存工具调用结果
                         self.cache.set(combined_question, 'function_knowledge', 'tool_result.json', tool_result)
-                        
-                        execution_record["execution"]["steps"].append({
-                            "step": 4,
-                            "name": "call_tool",
-                            "duration": time.time() - step4_start,
-                            "output": "success"
-                        })
                 else:
-                    step4_start = time.time()
                     logger.info(f"  → 调用工具,参数: {arguments}")
                     tool_result = call_tool(tool_name, arguments)
-                    execution_record["execution"]["steps"].append({
-                        "step": 4,
-                        "name": "call_tool",
-                        "duration": time.time() - step4_start,
-                        "output": "success"
-                    })
-                
-                # 记录工具调用信息
-                execution_record["execution"]["tool_info"] = {
-                    "tool_name": tool_name,
-                    "parameters": arguments,
-                    "result": tool_result
-                }
-                
-                result_content = f"工具 {tool_name} 执行结果: {json.dumps(tool_result, ensure_ascii=False)}"
-                execution_record["result"]["content"] = result_content
-                execution_record["result"]["raw_data"] = tool_result
                 
                 logger.info(f"✓ 工具调用完成")
                 
             else:
                 # 路径B: 知识搜索
-                execution_record["result"]["type"] = "knowledge_search"
-                
                 logger.info("[步骤4] 未找到合适工具,调用 MultiSearch...")
                 
-                step4_start = time.time()
                 knowledge = get_multi_search_knowledge(query, cache_key=combined_question)
                 
-                execution_record["execution"]["steps"].append({
-                    "step": 4,
-                    "name": "multi_search_knowledge",
-                    "duration": time.time() - step4_start,
-                    "output": f"knowledge_length: {len(knowledge)}"
-                })
-                
-                # 记录知识搜索信息
-                execution_record["execution"]["knowledge_search_info"] = {
-                    "query": query,
-                    "knowledge_length": len(knowledge),
-                    "source": "multi_search"
-                }
-                
-                result_content = knowledge
-                execution_record["result"]["content"] = knowledge
-                execution_record["result"]["raw_data"] = {"knowledge": knowledge, "query": query}
-                
-                # 异步生成新工具
-                logger.info("[后台任务] 启动新工具生成线程...")
+                # 异步保存知识到文件
+                logger.info("[后台任务] 保存知识到文件...")
                 threading.Thread(target=self.save_knowledge_to_file, args=(knowledge, combined_question)).start()
-            # 计算总执行时间
-            execution_record["metadata"]["execution_time"] = time.time() - start_time
             
-            # 保存完整的执行记录到JSON文件
-            if self.use_cache:
-                self.cache.set(combined_question, 'function_knowledge', 'final_result.json', execution_record)
-                
-                # 同时保存一个格式化的JSON文件供人类阅读
-                from knowledge_v2.cache_manager import CacheManager
-                cache = CacheManager()
-                import hashlib
-                question_hash = hashlib.md5(combined_question.encode('utf-8')).hexdigest()[:12]
-                output_file = os.path.join(cache.base_cache_dir, question_hash, 'execution_record.json')
-                
-                try:
-                    with open(output_file, 'w', encoding='utf-8') as f:
-                        json.dump(execution_record, f, ensure_ascii=False, indent=2)
-                    logger.info(f"✓ 完整执行记录已保存: {output_file}")
-                except Exception as e:
-                    logger.error(f"保存执行记录失败: {e}")
+            # 计算执行时间并保存详情
+            self.execution_detail["execution_time"] = time.time() - start_time
+            self._save_execution_detail(combined_question)
+            
+            # 收集所有执行记录
+            logger.info("=" * 80)
+            logger.info("收集执行记录...")
+            logger.info("=" * 80)
+            
+            from knowledge_v2.execution_collector import collect_and_save_execution_record
+            
+            execution_record = collect_and_save_execution_record(
+                combined_question,
+                {
+                    "question": question,
+                    "post_info": post_info,
+                    "persona_info": persona_info,
+                    "timestamp": timestamp
+                }
+            )
             
             logger.info("=" * 80)
             logger.info(f"✓ Function Knowledge 完成")
-            logger.info(f"  类型: {execution_record['result']['type']}")
-            logger.info(f"  结果长度: {len(result_content) if result_content else 0}")
-            logger.info(f"  执行时间: {execution_record['metadata']['execution_time']:.2f}秒")
+            logger.info(f"  执行时间: {execution_record.get('metadata', {}).get('execution_time', 0):.2f}秒")
             logger.info("=" * 80 + "\n")
             
             return execution_record
@@ -492,27 +432,42 @@ class FunctionKnowledge:
         except Exception as e:
             logger.error(f"✗ 执行失败: {e}")
             import traceback
-            error_trace = traceback.format_exc()
-            
-            execution_record["metadata"]["errors"].append({
-                "error": str(e),
-                "traceback": error_trace
-            })
-            execution_record["result"]["type"] = "error"
-            execution_record["result"]["content"] = f"执行失败: {str(e)}"
-            execution_record["metadata"]["execution_time"] = time.time() - start_time
+            logger.error(traceback.format_exc())
             
-            return execution_record
-
-def get_knowledge(question: str, post_info: str, persona_info: str) -> dict:
-    """
-    便捷调用函数
-    
-    Returns:
-        dict: 完整的执行记录,包含输入、执行过程、结果和元数据
-    """
-    agent = FunctionKnowledge()
-    return agent.get_knowledge(question, post_info, persona_info)
+            # 即使失败也尝试保存详情和收集记录
+            try:
+                self.execution_detail["execution_time"] = time.time() - start_time
+                self._save_execution_detail(combined_question)
+                
+                from knowledge_v2.execution_collector import collect_and_save_execution_record
+                execution_record = collect_and_save_execution_record(
+                    combined_question,
+                    {
+                        "question": question,
+                        "post_info": post_info,
+                        "persona_info": persona_info,
+                        "timestamp": timestamp
+                    }
+                )
+                return execution_record
+            except Exception as collect_error:
+                logger.error(f"收集执行记录也失败: {collect_error}")
+                # 返回基本错误信息
+                return {
+                    "input": {
+                        "question": question,
+                        "post_info": post_info,
+                        "persona_info": persona_info,
+                        "timestamp": timestamp
+                    },
+                    "result": {
+                        "type": "error",
+                        "content": f"执行失败: {str(e)}"
+                    },
+                    "metadata": {
+                        "errors": [str(e)]
+                    }
+                }
 
 if __name__ == "__main__":
     # 测试代码
@@ -526,9 +481,7 @@ if __name__ == "__main__":
         print("=" * 50)
         print("执行结果:")
         print("=" * 50)
-        print(f"类型: {execution_result['result']['type']}")
-        print(f"内容预览: {execution_result['result']['content'][:200]}...")
-        print(f"执行时间: {execution_result['metadata']['execution_time']:.2f}秒")
+        print(json.dumps(execution_result, ensure_ascii=False, indent=2))
         print(f"\n完整JSON已保存到缓存目录")
     except Exception as e:
         logger.error(f"测试失败: {e}")

+ 149 - 1
knowledge_v2/llm_search_knowledge.py

@@ -43,6 +43,16 @@ class LLMSearchKnowledge:
         self.prompt_dir = os.path.join(current_dir, "prompt")
         self.use_cache = use_cache
         self.cache = CacheManager() if use_cache else None
+        
+        # 执行详情收集
+        self.execution_detail = {
+            "generate_queries": None,
+            "search_results": [],
+            "merge_detail": None,
+            "execution_time": 0,
+            "cache_hits": []
+        }
+        
         logger.info(f"缓存状态: {'启用' if use_cache else '禁用'}")
         logger.info("=" * 60)
         
@@ -100,6 +110,11 @@ class LLMSearchKnowledge:
             cached_queries = self.cache.get(question, 'llm_search', 'generated_queries.json')
             if cached_queries:
                 logger.info(f"✓ 使用缓存的queries: {cached_queries}")
+                # 记录缓存命中
+                self.execution_detail["generate_queries"].update({
+                    "cached": True,
+                    "queries_count": len(cached_queries)
+                })
                 return cached_queries
         
         try:
@@ -136,6 +151,15 @@ class LLMSearchKnowledge:
                 for i, q in enumerate(queries, 1):
                     logger.info(f"  {i}. {q}")
                 
+                # 记录执行详情
+                self.execution_detail["generate_queries"].update({
+                    "cached": False,
+                    "prompt": prompt,
+                    "response": response_text,
+                    "queries_count": len(queries),
+                    "queries": queries
+                })
+                
                 # 写入缓存
                 if self.use_cache:
                     self.cache.set(question, 'llm_search', 'generated_queries.json', queries)
@@ -174,6 +198,14 @@ class LLMSearchKnowledge:
             cached_result = self.cache.get(question, 'llm_search/search_results', cache_filename)
             if cached_result:
                 logger.info(f"  ✓ 使用缓存结果 (长度: {len(cached_result)})")
+                # 记录缓存命中
+                self.execution_detail["search_results"].append({
+                    "query": query,
+                    "query_index": query_index,
+                    "cached": True,
+                    "result_length": len(cached_result)
+                })
+                self.execution_detail["cache_hits"].append(f"search_result_{query_index:03d}")
                 return cached_result
         
         try:
@@ -193,6 +225,14 @@ class LLMSearchKnowledge:
             
             logger.info(f"  ✓ 获取知识文本 (长度: {len(knowledge_text)})")
             
+            # 记录搜索结果详情
+            self.execution_detail["search_results"].append({
+                "query": query,
+                "query_index": query_index,
+                "cached": False,
+                "result_length": len(knowledge_text)
+            })
+            
             # 写入缓存
             if self.use_cache:
                 cache_filename = f"search_result_{query_index:03d}.txt"
@@ -248,12 +288,18 @@ class LLMSearchKnowledge:
 
         if len(knowledge_texts) == 1:
             return knowledge_texts[0]
-        
+
         # 尝试从缓存读取
         if self.use_cache:
             cached_merged = self.cache.get(question, 'llm_search', 'merged_knowledge.txt')
             if cached_merged:
                 logger.info(f"✓ 使用缓存的合并知识 (长度: {len(cached_merged)})")
+                # 记录缓存命中
+                self.execution_detail["merge_detail"].update({
+                    "cached": True,
+                    "knowledge_count": len(knowledge_texts),
+                    "result_length": len(cached_merged)
+                })
                 return cached_merged
         
         try:
@@ -289,6 +335,15 @@ class LLMSearchKnowledge:
             
             logger.info(f"✓ 成功合并知识文本 (长度: {len(merged_text)})")
             
+            # 记录合并详情
+            self.execution_detail["merge_detail"].update({
+                "cached": False,
+                "prompt": prompt,
+                "response": merged_text,
+                "knowledge_count": len(knowledge_texts),
+                "result_length": len(merged_text)
+            })
+            
             # 写入缓存
             if self.use_cache:
                 self.cache.set(question, 'llm_search', 'merged_knowledge.txt', merged_text.strip())
@@ -298,6 +353,88 @@ class LLMSearchKnowledge:
         except Exception as e:
             logger.error(f"✗ 合并知识文本失败: {e}")
             raise
+
+    def _save_execution_detail(self, cache_key: str):
+        """
+        保存执行详情到缓存(支持合并旧记录)
+
+        Args:
+            cache_key: 缓存键
+        """
+        if not self.use_cache or not self.cache:
+            return
+
+        try:
+            import hashlib
+            question_hash = hashlib.md5(cache_key.encode('utf-8')).hexdigest()[:12]
+            detail_dir = os.path.join(
+                self.cache.base_cache_dir,
+                question_hash,
+                'llm_search'
+            )
+            os.makedirs(detail_dir, exist_ok=True)
+
+            detail_file = os.path.join(detail_dir, 'execution_detail.json')
+
+            # 准备最终要保存的数据
+            final_detail = self.execution_detail.copy()
+
+            # 尝试读取旧文件进行合并
+            if os.path.exists(detail_file):
+                try:
+                    with open(detail_file, 'r', encoding='utf-8') as f:
+                        old_detail = json.load(f)
+
+                    # 1. 合并 generate_queries
+                    new_gen = self.execution_detail.get("generate_queries")
+                    old_gen = old_detail.get("generate_queries")
+                    if (new_gen and isinstance(new_gen, dict) and
+                            new_gen.get("cached") is True and
+                            old_gen and isinstance(old_gen, dict) and
+                            "prompt" in old_gen):
+                        final_detail["generate_queries"] = old_gen
+
+                    # 2. 合并 merge_detail
+                    new_merge = self.execution_detail.get("merge_detail")
+                    old_merge = old_detail.get("merge_detail")
+                    if (new_merge and isinstance(new_merge, dict) and
+                            new_merge.get("cached") is True and
+                            old_merge and isinstance(old_merge, dict) and
+                            "prompt" in old_merge):
+                        final_detail["merge_detail"] = old_merge
+
+                    # 3. 合并 search_results (列表)
+                    new_results = self.execution_detail.get("search_results", [])
+                    old_results = old_detail.get("search_results", [])
+
+                    if new_results and old_results:
+                        merged_results = []
+                        # 建立旧结果的索引:(query, index) -> item
+                        old_map = {(item.get("query"), item.get("query_index")): item
+                                   for item in old_results if isinstance(item, dict)}
+
+                        for item in new_results:
+                            if item.get("cached") is True:
+                                key = (item.get("query"), item.get("query_index"))
+                                if key in old_map:
+                                    # 如果旧项包含更多信息(例如非cached状态),则使用旧项
+                                    old_item = old_map[key]
+                                    if old_item.get("cached") is False:
+                                        merged_results.append(old_item)
+                                        continue
+                            merged_results.append(item)
+                        final_detail["search_results"] = merged_results
+
+                except Exception as e:
+                    logger.warning(f"  ⚠ 读取旧详情失败: {e}")
+
+            with open(detail_file, 'w', encoding='utf-8') as f:
+                json.dump(final_detail, f, ensure_ascii=False, indent=2)
+
+            logger.info(f"✓ 执行详情已保存: {detail_file}")
+
+        except Exception as e:
+            logger.error(f"✗ 保存执行详情失败: {e}")
     
     def get_knowledge(self, question: str, cache_key: str = None, need_generate_query: bool = True) -> str:
         """
@@ -316,6 +453,9 @@ class LLMSearchKnowledge:
         # 使用cache_key或question作为缓存键
         actual_cache_key = cache_key if cache_key is not None else question
         
+        import time
+        start_time = time.time()
+        
         try:
             logger.info(f"{'='*60}")
             logger.info(f"LLM Search - 开始处理问题: {question[:50]}...")
@@ -336,10 +476,18 @@ class LLMSearchKnowledge:
             logger.info(f"{'='*60}")
             logger.info(f"✓ LLM Search 完成 (最终长度: {len(merged_knowledge)})")
             logger.info(f"{'='*60}\n")
+            
+            # 计算执行时间并保存详情
+            self.execution_detail["execution_time"] = time.time() - start_time
+            self._save_execution_detail(actual_cache_key)
+            
             return merged_knowledge
             
         except Exception as e:
             logger.error(f"✗ 获取知识文本失败,问题: {question[:50]}..., 错误: {e}")
+            # 即使失败也保存执行详情
+            self.execution_detail["execution_time"] = time.time() - start_time
+            self._save_execution_detail(actual_cache_key)
             raise
 
 

+ 94 - 1
knowledge_v2/multi_search_knowledge.py

@@ -39,6 +39,15 @@ class MultiSearchKnowledge:
         self.prompt_dir = os.path.join(current_dir, "prompt")
         self.use_cache = use_cache
         self.cache = CacheManager() if use_cache else None
+        
+        # 执行详情收集
+        self.execution_detail = {
+            "sources": {},
+            "merge_detail": None,
+            "execution_time": 0,
+            "cache_hits": []
+        }
+        
         logger.info(f"缓存状态: {'启用' if use_cache else '禁用'}")
         logger.info("=" * 60)
         
@@ -89,6 +98,13 @@ class MultiSearchKnowledge:
             cached_merged = self.cache.get(question, 'multi_search', 'merged_knowledge.txt')
             if cached_merged:
                 logger.info(f"✓ 使用缓存的合并知识 (长度: {len(cached_merged)})")
+                # 记录缓存命中
+                self.execution_detail["merge_detail"].update({
+                    "cached": True,
+                    "sources_count": len(knowledge_map),
+                    "result_length": len(cached_merged)
+                })
+                self.execution_detail["cache_hits"].append("merged_knowledge")
                 return cached_merged
         
         try:
@@ -119,6 +135,16 @@ class MultiSearchKnowledge:
             
             logger.info(f"✓ 多渠道知识合并完成 (长度: {len(merged_text)})")
             
+            # 记录合并详情
+            self.execution_detail["merge_detail"].update({
+                "cached": False,
+                "prompt": prompt,
+                "response": merged_text,
+                "sources_count": len(knowledge_map),
+                "valid_sources_count": len(valid_knowledge),
+                "result_length": len(merged_text)
+            })
+            
             # 写入缓存
             if self.use_cache:
                 self.cache.set(question, 'multi_search', 'merged_knowledge.txt', merged_text.strip())
@@ -128,6 +154,53 @@ class MultiSearchKnowledge:
         except Exception as e:
             logger.error(f"✗ 合并知识失败: {e}")
             raise
+    
+    def _save_execution_detail(self, cache_key: str):
+        """保存执行详情到缓存(支持合并旧记录)"""
+        if not self.use_cache or not self.cache:
+            return
+        
+        try:
+            import hashlib
+            question_hash = hashlib.md5(cache_key.encode('utf-8')).hexdigest()[:12]
+            detail_dir = os.path.join(
+                self.cache.base_cache_dir,
+                question_hash,
+                'multi_search'
+            )
+            os.makedirs(detail_dir, exist_ok=True)
+            
+            detail_file = os.path.join(detail_dir, 'execution_detail.json')
+            
+            # 准备最终要保存的数据
+            final_detail = self.execution_detail.copy()
+            
+            # 尝试读取旧文件进行合并
+            if os.path.exists(detail_file):
+                try:
+                    with open(detail_file, 'r', encoding='utf-8') as f:
+                        old_detail = json.load(f)
+                    
+                    # 合并 merge_detail
+                    new_merge = self.execution_detail.get("merge_detail")
+                    old_merge = old_detail.get("merge_detail")
+                    
+                    if (new_merge and isinstance(new_merge, dict) and 
+                        new_merge.get("cached") is True and 
+                        old_merge and isinstance(old_merge, dict) and 
+                        "prompt" in old_merge):
+                        final_detail["merge_detail"] = old_merge
+                        
+                except Exception as e:
+                    logger.warning(f"  ⚠ 读取旧详情失败: {e}")
+
+            with open(detail_file, 'w', encoding='utf-8') as f:
+                json.dump(final_detail, f, ensure_ascii=False, indent=2)
+            
+            logger.info(f"✓ 执行详情已保存: {detail_file}")
+            
+        except Exception as e:
+            logger.error(f"✗ 保存执行详情失败: {e}")
 
     def get_knowledge(self, question: str, cache_key: str = None) -> str:
         """
@@ -140,9 +213,12 @@ class MultiSearchKnowledge:
         Returns:
             str: 最终的知识文本
         """
-        # 使用cache_key或question作为缓存键
+        #使用cache_key或question作为缓存键
         actual_cache_key = cache_key if cache_key is not None else question
         
+        import time
+        start_time = time.time()
+        
         logger.info(f"{'='*60}")
         logger.info(f"Multi-Search - 开始处理问题: {question[:50]}...")
         logger.info(f"{'='*60}")
@@ -153,6 +229,10 @@ class MultiSearchKnowledge:
             if cached_final:
                 logger.info(f"✓ 使用缓存的最终知识 (长度: {len(cached_final)})")
                 logger.info(f"{'='*60}\n")
+                # 记录缓存命中
+                self.execution_detail["cache_hits"].append("final_knowledge")
+                self.execution_detail["execution_time"] = time.time() - start_time
+                self._save_execution_detail(actual_cache_key)
                 return cached_final
         
         knowledge_map = {}
@@ -163,9 +243,18 @@ class MultiSearchKnowledge:
             llm_knowledge = get_llm_knowledge(question, cache_key=actual_cache_key)
             knowledge_map["LLM Search"] = llm_knowledge
             logger.info(f"✓ LLM Search 完成 (长度: {len(llm_knowledge)})")
+            # 记录来源详情
+            self.execution_detail["sources"]["llm_search"] = {
+                "success": True,
+                "knowledge_length": len(llm_knowledge)
+            }
         except Exception as e:
             logger.error(f"✗ LLM Search 失败: {e}")
             knowledge_map["LLM Search"] = ""
+            self.execution_detail["sources"]["llm_search"] = {
+                "success": False,
+                "error": str(e)
+            }
             
         # 2. 获取 XHS Search 知识 (暂时注释)
         # try:
@@ -187,6 +276,10 @@ class MultiSearchKnowledge:
         logger.info(f"✓ Multi-Search 完成 (最终长度: {len(final_knowledge)})")
         logger.info(f"{'='*60}\n")
         
+        # 计算执行时间并保存详情
+        self.execution_detail["execution_time"] = time.time() - start_time
+        self._save_execution_detail(actual_cache_key)
+        
         return final_knowledge
 
 def get_knowledge(question: str, cache_key: str = None) -> str:

+ 0 - 56
knowledge_v2/test_detail_info.py

@@ -1,56 +0,0 @@
-"""
-快速测试 - 验证detail_info初始化
-"""
-
-import os
-import sys
-
-# 添加路径
-current_dir = os.path.dirname(os.path.abspath(__file__))
-root_dir = os.path.dirname(current_dir)
-sys.path.insert(0, root_dir)
-
-print("测试: detail_info初始化验证")
-print("=" * 60)
-
-try:
-    from knowledge_v2.function_knowledge import FunctionKnowledge
-    
-    # 创建实例(禁用缓存以测试所有路径)
-    agent = FunctionKnowledge(use_cache=False)
-    
-    # 测试1: generate_query
-    print("\n[测试1] generate_query")
-    query, detail = agent.generate_query("测试问题", "无", "测试")
-    print(f"✓ 返回类型: {type(query)}, {type(detail)}")
-    print(f"✓ detail keys: {detail.keys()}")
-    assert 'prompt' in detail, "detail应该包含prompt"
-    assert 'response' in detail, "detail应该包含response"
-    print("✓ generate_query 通过")
-    
-    # 测试2: select_tool  
-    print("\n[测试2] select_tool")
-    combined = "测试||无||测试"
-    tool_name, detail = agent.select_tool(combined, query)
-    print(f"✓ 返回类型: {type(tool_name)}, {type(detail)}")
-    print(f"✓ detail keys: {detail.keys()}")
-    assert 'prompt' in detail, "detail应该包含prompt"
-    assert 'response' in detail, "detail应该包含response"
-    print("✓ select_tool 通过")
-    
-    # 测试3: extract_tool_params
-    print("\n[测试3] extract_tool_params")
-    params, detail = agent.extract_tool_params(combined, "test_tool", query)
-    print(f"✓ 返回类型: {type(params)}, {type(detail)}")
-    print(f"✓ detail keys: {detail.keys()}")
-    assert 'prompt' in detail or 'fallback' in detail, "detail应该包含prompt或fallback"
-    print("✓ extract_tool_params 通过")
-    
-    print("\n" + "=" * 60)
-    print("✓ 所有测试通过!detail_info初始化正确")
-    print("=" * 60)
-    
-except Exception as e:
-    print(f"\n✗ 测试失败: {e}")
-    import traceback
-    traceback.print_exc()

+ 21 - 0
knowledge_v2/tool_infos/wechat_search_article.txt

@@ -0,0 +1,21 @@
+{
+    "name": "wechat_search_article",
+    "title": "WeChat Official Account Article Search",
+    "description": "Search for articles from WeChat official accounts using keywords. Supports pagination via cursor-based navigation.",
+    "inputSchema": {
+        "type": "object",
+        "properties": {
+            "keyword": {
+                "type": "string",
+                "description": "Search keyword, required for querying articles"
+            },
+            "cursor": {
+                "type": "string",
+                "description": "Pagination cursor. Leave empty for first page, use value from previous response for subsequent pages"
+            }
+        },
+        "required": [
+            "keyword"
+        ]
+    }
+}

Някои файлове не бяха показани, защото твърде много файлове са промени