feat: 添加 context_rewrite 工具并清理工具限制器死配置

- 新增 context_rewrite 工具:允许 AI 在单轮内压缩工具调用上下文
- 清理 persona_query_max 和 task_query_max 死配置(commit 9be60ba 引入)
- 同步全栈:后端/前端/文档/测试 18 个文件
- 测试:70/70 通过
This commit is contained in:
root
2026-04-16 14:33:21 +08:00
parent 535fca933a
commit 2aa6e9240f
19 changed files with 431 additions and 129 deletions

View File

@ -89,6 +89,7 @@
| `memory_commit` | 写入记忆 | 存储重要信息 |
| `memory_purge` | 删除记忆 | 修正错误信息 |
| `memory_introspect` | 查看状态 | 监控记忆系统 |
| `context_rewrite` | 压缩工具调用上下文 | 工具调用≥2次后压缩JSON为自然语言摘要 |
### 人设工具
| 工具 | 功能 | 使用场景 |
@ -104,6 +105,29 @@
| `task_delete` | 删除任务 | 清理完成任务 |
| `task_link_info` | 关联信息 | 连接任务与记忆 |
## context_rewrite 使用规则
当你已经执行了多次工具调用,且:
- 工具结果的JSON细节你已经理解不再需要原始格式
- 但你需要记住"我调用了哪些工具、得到了什么结论"
- 继续携带原始JSON会干扰后续推理
→ 调用 context_rewrite 压缩上下文
**强制格式要求**
- 必须标注 `[工具调用总结: 本次总结了 N 次工具调用 | 调用工具: tool1, tool2]`
- 必须保留关键语义信息
- 不可删除用户原始消息
- 不可歪曲工具返回的关键事实
**示例**
```
[工具调用总结: 本次总结了 2 次工具调用 | 调用工具: memory_recall, memory_recall]
- 查询人设图:未找到人设,使用默认身份
- 查询工作记忆链:发现 Task_成语接龙状态已暂停当前成语为虎作伥
```
## 每轮对话强制要求
### ⚠️ 执行顺序(每轮必须)

View File

@ -60,10 +60,8 @@ class BackendServer:
self._lock = threading.Lock()
self._config = {"api_key": "", "base_url": "https://api.deepseek.com", "model": "deepseek-chat"}
self._tool_limits = {
"persona_query_max": 1,
"persona_update_max": 1,
"task_query_max": 4,
"task_update_max": 2,
"task_update_max": 5,
"memory_query_max": 20,
"memory_update_max": 10,
}
@ -119,9 +117,7 @@ class BackendServer:
def _create_tool_limiter(self):
from .tool_limiter import ToolLimiter, ToolLimits
limits = ToolLimits(
persona_query_max=self._tool_limits.get("persona_query_max", 1),
persona_update_max=self._tool_limits.get("persona_update_max", 1),
task_query_max=self._tool_limits.get("task_query_max", 4),
task_update_max=self._tool_limits.get("task_update_max", 5),
memory_query_max=self._tool_limits.get("memory_query_max", 20),
memory_update_max=self._tool_limits.get("memory_update_max", 10),
@ -243,6 +239,25 @@ class BackendServer:
self._tool_limiter.record_call(tool_call.function.name, args)
if tool_call.function.name == "context_rewrite":
result = execute_tool(self._graph, tool_call.function.name, args)
result_data = json.loads(result)
if result_data.get("status") == "success":
user_msg = messages_history[0]
messages_history[:] = [
user_msg,
{"role": "assistant", "content": result_data["summary"]}
]
tool_result_msg = {
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
}
current_tool_results.append(tool_result_msg)
continue
result = execute_tool(self._graph, tool_call.function.name, args)
tool_calls.append({
"name": tool_call.function.name,
@ -324,8 +339,8 @@ class BackendServer:
self.update_config(api_key, base_url, model)
limits_keys = [
"persona_query_max", "persona_update_max",
"task_query_max", "task_update_max",
"persona_update_max",
"task_update_max",
"memory_query_max", "memory_update_max"
]
for key in limits_keys:

View File

@ -50,6 +50,10 @@ def execute_tool(graph: Any, tool_name: str, arguments: dict) -> str:
result = graph.cleanup(dry_run=arguments.get("dry_run", True))
return json.dumps(result, ensure_ascii=False, default=str)
elif tool_name == "context_rewrite":
result = execute_context_rewrite(graph, arguments)
return json.dumps(result, ensure_ascii=False, default=str)
# 人设图管理工具
elif tool_name == "persona_update":
result = execute_persona_update(graph, arguments)
@ -111,6 +115,24 @@ def format_recall_result(result: dict) -> str:
return "\n".join(lines)
def execute_context_rewrite(graph: Any, arguments: dict) -> dict:
"""压缩工具调用上下文"""
summary = arguments.get("summary", "")
# 验证格式:必须包含工具调用标记
if "[工具调用总结" not in summary:
return {
"status": "error",
"message": "总结格式错误:必须包含 [工具调用总结: 本次总结了 N 次工具调用 | 调用工具: ...] 标记"
}
return {
"status": "success",
"message": "上下文已压缩",
"summary": summary
}
# 人设图管理工具实现
def execute_persona_update(graph: Any, arguments: dict) -> dict:
"""更新人设"""

View File

@ -1,49 +1,35 @@
"""
工具调用限制器 - 限制每轮对话中各类工具的调用次数
"""
from typing import Dict, List, Optional
from dataclasses import dataclass, field
from typing import Optional
from dataclasses import dataclass
@dataclass
class ToolLimits:
"""工具调用限制配置"""
# 人设图限制
persona_query_max: int = 1 # 每轮最多查询1次人设图
persona_update_max: int = 1 # 每轮最多修改1次人设图
# 工作记忆链限制
task_query_max: int = 4 # 每轮最多查询4次工作记忆链
task_update_max: int = 5 # 每轮最多修改5次工作记忆链
# 一般记忆限制
memory_query_max: int = 20 # 每轮最多查询20次一般记忆
memory_update_max: int = 10 # 每轮最多修改10次一般记忆
persona_update_max: int = 1
task_update_max: int = 5
memory_query_max: int = 20
memory_update_max: int = 10
@dataclass
class ToolCallCount:
"""工具调用计数"""
# 人设图
persona_query: int = 0
persona_update: int = 0
# 工作记忆链
task_query: int = 0
task_update: int = 0
# 一般记忆
memory_query: int = 0
memory_update: int = 0
class ToolLimiter:
"""工具调用限制器"""
def __init__(self, limits: Optional[ToolLimits] = None):
self.limits = limits or ToolLimits()
self.counts = ToolCallCount()
def _classify_tool(self, tool_name: str, arguments: dict) -> tuple:
"""
分类工具调用
@ -51,105 +37,83 @@ class ToolLimiter:
category: 'persona', 'task', 'memory'
operation: 'query', 'update'
"""
# 人设图工具
if tool_name in ('persona_update', 'persona_clear'):
return ('persona', 'update')
# 工作记忆链工具
if tool_name in ('task_create', 'task_set_state', 'task_delete', 'task_link_info'):
# task_link_info 是关联操作,算作更新
return ('task', 'update')
# 一般记忆工具
if tool_name == 'memory_recall':
# 所有 memory_recall 统一归为一般记忆查询
# 因为 query_intent 内容不可控,无法准确判断查询类型
# 写入操作通过工具名称明确区分,不受此影响
return ('memory', 'query')
if tool_name == 'memory_commit':
return ('memory', 'update')
if tool_name == 'memory_purge':
return ('memory', 'update')
if tool_name == 'memory_introspect':
return ('memory', 'query')
if tool_name in ('memory_archive', 'memory_cleanup'):
return ('memory', 'update')
# 未知工具,归类为一般记忆更新
if tool_name == 'context_rewrite':
return ('memory', 'query')
return ('memory', 'update')
def can_call(self, tool_name: str, arguments: dict) -> tuple:
"""
检查是否允许调用工具
返回: (allowed, reason)
"""
category, operation = self._classify_tool(tool_name, arguments)
# 获取当前计数和限制
if category == 'persona':
if operation == 'query':
if self.counts.persona_query >= self.limits.persona_query_max:
return (False, f"人设图查询次数已达上限({self.limits.persona_query_max}次)")
else: # update
if self.counts.persona_update >= self.limits.persona_update_max:
return (False, f"人设图修改次数已达上限({self.limits.persona_update_max}次)")
if self.counts.persona_update >= self.limits.persona_update_max:
return (False, f"人设图修改次数已达上限({self.limits.persona_update_max}次)")
elif category == 'task':
if operation == 'query':
if self.counts.task_query >= self.limits.task_query_max:
return (False, f"工作记忆链查询次数已达上限({self.limits.task_query_max}次)")
else: # update
if self.counts.task_update >= self.limits.task_update_max:
return (False, f"工作记忆链修改次数已达上限({self.limits.task_update_max}次)")
if self.counts.task_update >= self.limits.task_update_max:
return (False, f"工作记忆链修改次数已达上限({self.limits.task_update_max}次)")
elif category == 'memory':
if operation == 'query':
if self.counts.memory_query >= self.limits.memory_query_max:
return (False, f"一般记忆查询次数已达上限({self.limits.memory_query_max}次)")
else: # update
else:
if self.counts.memory_update >= self.limits.memory_update_max:
return (False, f"一般记忆修改次数已达上限({self.limits.memory_update_max}次)")
return (True, "允许调用")
def record_call(self, tool_name: str, arguments: dict) -> None:
"""记录工具调用"""
category, operation = self._classify_tool(tool_name, arguments)
if category == 'persona':
if operation == 'query':
self.counts.persona_query += 1
else:
self.counts.persona_update += 1
self.counts.persona_update += 1
elif category == 'task':
if operation == 'query':
self.counts.task_query += 1
else:
self.counts.task_update += 1
self.counts.task_update += 1
elif category == 'memory':
if operation == 'query':
self.counts.memory_query += 1
else:
self.counts.memory_update += 1
def get_summary(self) -> str:
"""获取调用统计摘要"""
lines = [
f"人设图: 查询{self.counts.persona_query}/{self.limits.persona_query_max}, "
f"修改{self.counts.persona_update}/{self.limits.persona_update_max}",
f"工作记忆链: 查询{self.counts.task_query}/{self.limits.task_query_max}次, "
f"修改{self.counts.task_update}/{self.limits.task_update_max}",
f"人设图: 修改{self.counts.persona_update}/{self.limits.persona_update_max}",
f"工作记忆链: 修改{self.counts.task_update}/{self.limits.task_update_max}",
f"一般记忆: 查询{self.counts.memory_query}/{self.limits.memory_query_max}次, "
f"修改{self.counts.memory_update}/{self.limits.memory_update_max}"
]
return "\n".join(lines)
def reset(self) -> None:
"""重置计数(新的一轮对话开始时调用)"""
self.counts = ToolCallCount()

View File

@ -234,6 +234,43 @@ MEMORY_TOOLS = [
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "context_rewrite",
"description": """压缩本轮对话的工具调用上下文。将冗长的JSON工具结果提炼为简洁摘要。
【使用场景】
- 已执行多次工具调用JSON细节已理解不再需要原始格式
- 但需保留"我调用了什么工具、得到了什么结论"的元认知
- 继续携带原始JSON会干扰后续推理
【⚠️ 强制格式要求】
1. 必须标注调用了哪些工具
2. 必须标注是对几次工具调用的总结
3. 必须保留关键语义信息
【示例】
{
"summary": "[工具调用总结: 本次总结了 2 次工具调用 | 调用工具: memory_recall, memory_recall]\\n\\n- 查询人设图:未找到人设,使用默认身份\\n- 查询工作记忆链:发现 Task_成语接龙状态已暂停当前成语为虎作伥"
}
【注意事项】
- 不可删除用户原始消息
- 不可歪曲工具返回的关键事实
- 仅在工具调用 ≥ 2 次后使用""",
"parameters": {
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "压缩后的摘要文本,必须包含工具调用元信息"
}
},
"required": ["summary"]
}
}
}
]