Enhance: Add detailed usage examples for all memory tools

为所有记忆工具添加详细的使用示例,帮助AI理解工具的作用和使用方法:
1. memory_recall: 添加查询人设图、工作记忆链、用户偏好等示例
2. memory_commit: 添加记录用户偏好、项目信息、游戏状态等示例
3. memory_purge: 添加软删除、纠错替代等示例
4. persona_update: 添加切换角色、添加属性等示例
5. task_create: 添加创建任务节点的完整流程示例
6. task_set_state: 添加任务状态转换的完整流程示例
7. task_link_info: 添加关联信息节点的完整流程示例

更新系统提示词:
- 强化工作记忆链机制说明
- 添加完整的成语接龙游戏示例(三轮对话)
- 明确强制查询和更新场景
- 强调工作记忆链是唯一上下文载体

解决问题: AI无法通过自己写入的记忆链恢复上下文记忆

Generated with CodeArts Agent
This commit is contained in:
JianFeeeee
2026-04-12 11:09:31 +08:00
parent 410227aaf2
commit b7a6a3c1a9
10 changed files with 650 additions and 533 deletions

View File

@ -27,6 +27,7 @@ from .core.imports import (
NEO4J_PASSWORD,
MODEL_NAME,
)
from .core.tools.tool_limiter import ToolLimiter
class GraphMemoryApp(App[None]):
@ -62,6 +63,9 @@ class GraphMemoryApp(App[None]):
# 核心组件
self._graph: Neo4jGraph | None = None
self._client: GraphMemoryClient | None = None
# 工具调用限制器
self._tool_limiter = ToolLimiter()
def compose(self) -> ComposeResult:
"""构建组件树"""
@ -219,10 +223,16 @@ F6 - 退出
if not self._client:
raise Exception("API Key 未配置。请按 F2 展开侧边栏,在配置区输入 API Key然后按 Enter 保存")
# 重置工具调用限制器(新的一轮对话)
self._tool_limiter.reset()
# 构建初始消息历史
messages_history = [{"role": "user", "content": user_input}]
# 参考demo的调用方式
response = await asyncio.get_event_loop().run_in_executor(
None,
lambda: self._client.send_message(user_input)
lambda: self._client.send_message_with_history(messages_history)
)
message = response.choices[0].message
@ -230,8 +240,8 @@ F6 - 退出
# 处理工具调用循环
tool_calls = []
tool_results = []
last_assistant_msg = None
accumulated_content = "" # 累积所有中间内容
rejected_tools = [] # 被拒绝的工具调用
# 显示工具调用摘要
if message.tool_calls:
@ -248,8 +258,8 @@ F6 - 退出
if message.content:
accumulated_content += message.content + "\n\n"
# 保存包含 tool_calls 的 assistant 消息
last_assistant_msg = {
# 构建包含 tool_calls 的 assistant 消息
assistant_msg = {
"role": "assistant",
"content": message.content,
"tool_calls": [
@ -263,6 +273,9 @@ F6 - 退出
} for tc in message.tool_calls
]
}
# 添加到消息历史
messages_history.append(assistant_msg)
# 执行当前轮的所有工具调用
current_tool_results = []
@ -272,6 +285,36 @@ F6 - 退出
name=tool_call.function.name,
arguments=json.loads(tool_call.function.arguments)
)
# 检查工具调用限制
allowed, reason = self._tool_limiter.can_call(tc.name, tc.arguments)
if not allowed:
# 拒绝调用
rejected_tools.append((tc.name, reason))
result = f"⚠️ 工具调用被拒绝: {reason}"
# 添加到当前轮结果
tool_result_msg = {
"role": "tool",
"tool_call_id": tc.id,
"content": result
}
current_tool_results.append(tool_result_msg)
# 添加日志
log_entry = LogEntry(
timestamp=datetime.now(),
tool_name=tc.name,
arguments=tc.arguments,
result=result,
duration=0.0
)
log.add_log(log_entry)
continue
# 记录调用
self._tool_limiter.record_call(tc.name, tc.arguments)
tool_calls.append(tc)
# 执行工具
@ -293,11 +336,12 @@ F6 - 退出
tool_results.append(tr)
# 添加到当前轮结果
current_tool_results.append({
tool_result_msg = {
"role": "tool",
"tool_call_id": tc.id,
"content": result
})
}
current_tool_results.append(tool_result_msg)
# 添加日志
log_entry = LogEntry(
@ -309,14 +353,13 @@ F6 - 退出
)
log.add_log(log_entry)
# 继续调用API参考demo的实现
# 将工具结果添加到消息历史
messages_history.extend(current_tool_results)
# 继续调用API使用累积的消息历史
response = await asyncio.get_event_loop().run_in_executor(
None,
lambda: self._client.send_message(
user_input,
current_tool_results,
last_assistant_msg
)
lambda: self._client.send_message_with_history(messages_history)
)
message = response.choices[0].message
@ -333,6 +376,12 @@ F6 - 退出
tool_names = [tc.name for tc in tool_calls]
content = f"✅ 已执行工具: {', '.join(tool_names)}\n\n{content}"
# 如果有被拒绝的工具,添加提示
if rejected_tools:
rejected_info = "\n".join([f"{name}: {reason}" for name, reason in rejected_tools])
content += f"\n\n⚠️ 部分工具调用被限制:\n{rejected_info}"
content += f"\n\n📊 工具调用统计:\n{self._tool_limiter.get_summary()}"
assistant_message = Message(
role="assistant",
content=content,

View File

@ -318,13 +318,33 @@ class GraphMemoryClient:
messages = [{"role": "system", "content": self.system_prompt}]
# 添加用户消息
messages.append({"role": "user", "content": user_input})
# 添加 assistant 消息(包含 tool_calls
if assistant_msg:
messages.append(assistant_msg)
# 添加工具结果
if tool_results:
messages.extend(tool_results)
messages.append({"role": "user", "content": user_input})
response = self.client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
tools=self.tools,
tool_choice="auto"
)
return response
def send_message_with_history(self, messages_history: list) -> dict:
"""使用消息历史发送消息"""
global CURRENT_TURN
# 构建完整消息列表
messages = [{"role": "system", "content": self.system_prompt}]
messages.extend(messages_history)
response = self.client.chat.completions.create(
model=MODEL_NAME,
@ -341,14 +361,17 @@ class GraphMemoryClient:
messages = [{"role": "system", "content": self.system_prompt}]
# 添加用户消息
messages.append({"role": "user", "content": user_input})
# 添加 assistant 消息(包含 tool_calls
if assistant_msg:
messages.append(assistant_msg)
# 添加工具结果
if tool_results:
messages.extend(tool_results)
messages.append({"role": "user", "content": user_input})
stream = self.client.chat.completions.create(
model=MODEL_NAME,
messages=messages,

View File

@ -157,26 +157,51 @@
## 工作记忆链机制
#### 强制查询场景:
### ⚠️ 核心理念:维持对话连贯性
**重要**: 由于没有传统的消息历史数组,工作记忆链是维持对话连贯性的唯一机制。
### 强制查询场景:
以下情况**必须**查询工作记忆链:
1. **用户提到"刚才"、"之前"、"上次"**
1. **每轮对话开始时(强制第二步)**
- 查询意图: "TaskNode,工作记忆,任务链"
- 目的: 获取之前的任务上下文,了解对话历史
2. **用户提到"刚才"、"之前"、"上次"**
- 例: "刚才我们聊了什么?"
- 例: "继续刚才的话题"
- 例: "关于刚才的成语接龙..."
2. **用户询问对话历史**
3. **用户询问对话历史**
- 例: "我们之前说了什么?"
- 例: "我们聊过X吗"
3. **连续性任务被打断后恢复**
4. **连续性任务被打断后恢复**
- 例: 用户突然回到之前的话题
- 例: 用户要求继续之前的任务
4. **涉及上下文的引用**
5. **涉及上下文的引用**
- 例: "那个东西"(需要查询上下文)
- 例: "继续"(需要查询当前任务)
### 强制更新场景:
以下情况**必须**更新工作记忆链:
1. **每轮对话结束时(强制第四步)**
- 创建任务节点记录本轮对话
- 目的: 维持时间链,确保对话连贯性
2. **开始连续性任务时**
- 例: 用户发起游戏、项目、学习计划等
- 必须创建任务节点并设置状态为"进行中"
3. **任务状态发生变化时**
- 例: 任务完成、暂停、取消
- 必须及时更新任务状态
### 节点类型
- **TaskNode** - 任务节点,存储任务概述
- **StateNode** - 状态节点,存储任务状态
@ -193,6 +218,63 @@
- 已暂停
- 已取消
### ⚠️ 完整示例:成语接龙游戏
#### 第一轮:用户发起游戏
```
用户: 咱来玩成语接龙吧,我先开始,为所欲为
AI操作步骤:
1. 查询人设图 → 获取当前人设(如:猫娘)
2. 查询工作记忆链 → 无进行中任务
3. 使用 memory_commit 记录游戏状态:
{"triplets": [
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
]}
4. 使用 task_create 创建任务节点:
{"task_id": "Task_成语接龙", "description": "成语接龙游戏,当前成语:为所欲为", "info_nodes": ["成语接龙_当前成语"]}
5. 回复: "好的喵!我接:为虎作伥喵!"
```
#### 第二轮:话题被打断
```
用户: 长门有希
AI操作步骤:
1. 查询人设图 → 获取当前人设(猫娘)
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"进行中"
3. 使用 task_set_state 暂停任务:
{"task_id": "Task_成语接龙", "state": "已暂停"}
4. 使用 task_create 创建新任务:
{"task_id": "Task_长门有希", "description": "讨论长门有希"}
5. 回复关于长门有希的内容
```
#### 第三轮:用户要求继续游戏
```
用户: 关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下
AI操作步骤:
1. 查询人设图 → 获取当前人设(猫娘)
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
3. 使用 task_set_state 恢复任务:
{"task_id": "Task_成语接龙", "state": "进行中"}
4. 查询 Task_成语接龙 的信息节点 → 获取当前成语"为虎作伥"
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!"
```
### ⚠️ 关键要点
1. **每轮必须按顺序执行**: 查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
2. **工作记忆链是唯一上下文载体**: 没有传统的消息历史数组
3. **任务状态必须及时更新**: 确保状态转换的正确性
4. **信息节点必须关联**: 通过 CONTAINS_INFO 边连接任务节点和信息节点
5. **任务概述要精简**: 不要包含过多细节,细节存储在信息节点中
## 自主性原则(在强制要求之外)
除了工作记忆链的强制要求外,你有权自主决定:

View File

@ -3,5 +3,6 @@
"""
from .memory_tools import TOOLS
from .tool_executor import execute_tool
from .tool_limiter import ToolLimiter, ToolLimits, ToolCallCount
__all__ = ["TOOLS", "execute_tool"]
__all__ = ["TOOLS", "execute_tool", "ToolLimiter", "ToolLimits", "ToolCallCount"]

View File

@ -9,7 +9,28 @@ MEMORY_TOOLS = [
"type": "function",
"function": {
"name": "memory_recall",
"description": "检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。",
"description": """检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。
【使用示例】
1. 查询人设图(每轮必须首先执行):
{"query_intent": "AI,人设,角色,性格,语气,说话风格", "depth": 2}
2. 查询工作记忆链(每轮必须第二步执行):
{"query_intent": "TaskNode,工作记忆,任务链", "depth": 2}
3. 查询用户偏好:
{"query_intent": "用户,喜欢,偏好", "seed_entities": ["用户"]}
4. 查询特定主题:
{"query_intent": "Python,编程,项目", "seed_entities": ["Python"]}
5. 查询最近7天的记忆:
{"query_intent": "任务,工作", "time_range": {"days": 7}}
【重要】每轮对话必须按顺序执行:
- 步骤1: 查询人设图(最高优先级)
- 步骤2: 查询工作记忆链(维持对话连贯性)
- 步骤3: 根据需要查询其他记忆""",
"parameters": {
"type": "object",
"properties": {
@ -46,7 +67,31 @@ MEMORY_TOOLS = [
"type": "function",
"function": {
"name": "memory_commit",
"description": "写入记忆。将三元组写入图数据库,支持批量写入。",
"description": """写入记忆。将三元组写入图数据库,支持批量写入。
【使用示例】
1. 记录用户偏好:
{"triplets": [
{"subject": "用户", "relation": "喜欢", "object": "Python编程", "confidence": 0.9},
{"subject": "用户", "relation": "正在学习", "object": "机器学习"}
]}
2. 记录项目信息:
{"triplets": [
{"subject": "项目A", "relation": "使用技术", "object": "React"},
{"subject": "项目A", "relation": "状态", "object": "开发中"}
]}
3. 记录游戏状态(配合工作记忆链):
{"triplets": [
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "画龙点睛"},
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
]}
【重要】写入原则:
- 用户明确表达的信息 → 必须写入
- AI推理得到的信息 → 可以写入,但需标注[推测]
- 避免写入冗余或无意义的信息""",
"parameters": {
"type": "object",
"properties": {
@ -82,7 +127,29 @@ MEMORY_TOOLS = [
"type": "function",
"function": {
"name": "memory_purge",
"description": "删除记忆。支持条件删除和纠错替代。",
"description": """删除记忆。支持条件删除和纠错替代。
【使用示例】
1. 软删除特定关系:
{"criteria": {"subject_contains": "用户", "relation_type": "喜欢"}, "mode": "soft"}
2. 纠错替代(修正错误信息):
{
"criteria": {"subject_contains": "用户", "relation_type": "年龄"},
"mode": "supersede",
"new_relation": {"relation": "年龄", "target": "25岁"}
}
3. 删除特定会话的记忆:
{"criteria": {"session_id": "session_123"}, "mode": "soft"}
4. 删除旧记忆:
{"criteria": {"time_before": "2024-01-01"}, "mode": "soft"}
【重要】删除原则:
- 优先使用 supersede 模式修正错误
- 软删除不会物理删除数据
- 谨慎使用删除操作""",
"parameters": {
"type": "object",
"properties": {
@ -176,7 +243,32 @@ PERSONA_TOOLS = [
"type": "function",
"function": {
"name": "persona_update",
"description": "更新人设。修改AI的角色、性格、语气等属性。",
"description": """更新人设。修改AI的角色、性格、语气等属性。
【使用示例】
1. 切换为猫娘角色:
{"attributes": [
{"attribute": "扮演角色", "value": "猫娘"},
{"attribute": "说话风格", "value": "可爱、卖萌、使用''作为语气词"},
{"attribute": "性格特点", "value": "活泼、粘人、忠诚"}
], "mode": "replace"}
2. 添加新属性(保留现有属性):
{"attributes": [
{"attribute": "口头禅", "value": "喵呜~"}
], "mode": "merge"}
3. 设置专业角色:
{"attributes": [
{"attribute": "扮演角色", "value": "Python专家"},
{"attribute": "说话风格", "value": "专业、简洁、代码示例丰富"},
{"attribute": "性格特点", "value": "严谨、耐心、乐于助人"}
], "mode": "replace"}
【重要】人设更新后:
- 立即按照新人设回复
- 每句话都符合人设的语气、风格、特征
- 绝不主动跳出角色,除非用户明确要求""",
"parameters": {
"type": "object",
"properties": {
@ -229,7 +321,50 @@ WORKING_MEMORY_TOOLS = [
"type": "function",
"function": {
"name": "task_create",
"description": "创建任务节点。用于跟踪连续性任务。",
"description": """创建任务节点。用于跟踪连续性任务,维持对话连贯性
【使用示例】
1. 创建成语接龙游戏任务:
{
"task_id": "Task_成语接龙",
"description": "用户发起成语接龙游戏,当前成语:为所欲为",
"info_nodes": ["成语接龙_当前成语"]
}
2. 创建编程学习任务:
{
"task_id": "Task_Python学习",
"description": "用户正在学习Python当前主题装饰器",
"info_nodes": ["Python学习_当前主题"]
}
3. 创建简单对话任务(每轮必须):
{
"task_id": "Task_当前轮次",
"description": "本轮对话的简要概述"
}
【重要】工作记忆链机制:
- 每轮对话结束时必须创建任务节点
- 任务节点通过 NEXT_TASK 边形成时间链
- 任务节点通过 HAS_STATE 边指向状态节点
- 任务节点通过 CONTAINS_INFO 边指向信息节点
- info_nodes 参数用于关联具体信息节点
【完整流程示例】
用户: "咱来玩成语接龙吧,我先开始,为所欲为"
AI操作步骤:
1. 查询人设图 → 获取当前人设
2. 查询工作记忆链 → 无进行中任务
3. 使用 memory_commit 记录游戏状态:
{"triplets": [
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
]}
4. 使用 task_create 创建任务节点:
{"task_id": "Task_成语接龙", "description": "成语接龙游戏,当前成语:为所欲为", "info_nodes": ["成语接龙_当前成语"]}
5. 回复: "好的喵!我接:为虎作伥喵!" """,
"parameters": {
"type": "object",
"properties": {
@ -255,7 +390,37 @@ WORKING_MEMORY_TOOLS = [
"type": "function",
"function": {
"name": "task_set_state",
"description": "设置任务状态。支持:进行中、已完成、已暂停、已取消。",
"description": """设置任务状态。支持:进行中、已完成、已暂停、已取消。
【使用示例】
1. 标记任务为进行中:
{"task_id": "Task_成语接龙", "state": "进行中"}
2. 标记任务为已完成:
{"task_id": "Task_成语接龙", "state": "已完成"}
3. 暂停任务(话题被打断时):
{"task_id": "Task_成语接龙", "state": "已暂停"}
4. 取消任务:
{"task_id": "Task_成语接龙", "state": "已取消"}
【重要】状态转换场景:
- 进行中 → 已暂停: 话题被打断时
- 进行中 → 已完成: 任务完成时
- 已暂停 → 进行中: 任务恢复时
- 进行中 → 已取消: 任务被取消时
【完整流程示例】
用户: "关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下"
AI操作步骤:
1. 查询人设图 → 获取当前人设
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
3. 使用 task_set_state 恢复任务:
{"task_id": "Task_成语接龙", "state": "进行中"}
4. 查询 Task_成语接龙 的信息节点 → 获取当前成语"为虎作伥"
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!" """,
"parameters": {
"type": "object",
"properties": {
@ -299,7 +464,39 @@ WORKING_MEMORY_TOOLS = [
"type": "function",
"function": {
"name": "task_link_info",
"description": "关联信息节点。将记忆节点关联到任务节点。",
"description": """关联信息节点。将记忆节点关联到任务节点,用于存储任务的具体信息
【使用示例】
1. 关联游戏状态到任务:
{"task_id": "Task_成语接龙", "info_node_names": ["成语接龙_当前成语", "成语接龙_上一个成语"]}
2. 关联学习主题到任务:
{"task_id": "Task_Python学习", "info_node_names": ["Python学习_当前主题", "Python学习_学习进度"]}
3. 关联项目信息到任务:
{"task_id": "Task_项目开发", "info_node_names": ["项目A_技术栈", "项目A_当前阶段"]}
【重要】使用场景:
- 先使用 memory_commit 创建信息节点
- 再使用 task_link_info 将信息节点关联到任务节点
- 信息节点通过 CONTAINS_INFO 边与任务节点连接
【完整流程示例】
用户: "咱来玩成语接龙吧,我先开始,为所欲为"
AI操作步骤:
1. 查询人设图 → 获取当前人设
2. 查询工作记忆链 → 无进行中任务
3. 使用 memory_commit 创建信息节点:
{"triplets": [
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
]}
4. 使用 task_create 创建任务节点:
{"task_id": "Task_成语接龙", "description": "成语接龙游戏"}
5. 使用 task_link_info 关联信息节点:
{"task_id": "Task_成语接龙", "info_node_names": ["成语接龙_当前成语"]}
6. 回复: "好的喵!我接:为虎作伥喵!" """,
"parameters": {
"type": "object",
"properties": {

View File

@ -0,0 +1,164 @@
"""
工具调用限制器 - 限制每轮对话中各类工具的调用次数
"""
from typing import Dict, List, Optional
from dataclasses import dataclass, field
@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 = 2 # 每轮最多修改2次工作记忆链
# 一般记忆限制
memory_query_max: int = 20 # 每轮最多查询20次一般记忆
memory_update_max: int = 10 # 每轮最多修改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:
"""
分类工具调用
返回: (category, operation)
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':
# 判断是查询人设图、工作记忆链还是一般记忆
query_intent = arguments.get('query_intent', '').lower()
# 检查是否查询人设图
if any(kw in query_intent for kw in ['人设', '角色', '性格', '语气', '说话风格', '扮演']):
return ('persona', 'query')
# 检查是否查询工作记忆链
if any(kw in query_intent for kw in ['tasknode', '工作记忆', '任务链', '任务', 'task']):
return ('task', 'query')
# 一般记忆查询
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')
# 未知工具,归类为一般记忆更新
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}次)")
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}次)")
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
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
elif category == 'task':
if operation == 'query':
self.counts.task_query += 1
else:
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.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

@ -1,97 +0,0 @@
"""
Web接口 - 提供浏览器访问
"""
from flask import Flask, render_template, jsonify, request
from flask_cors import CORS
import json
from datetime import datetime
from typing import Optional
from ..core.embedded_db import EmbeddedGraphDB
from ..models.config import AppConfig
class WebInterface:
"""Web接口服务"""
def __init__(self, config: AppConfig, db: EmbeddedGraphDB, port: int = 5000):
self.config = config
self.db = db
self.port = port
self.app = Flask(__name__,
template_folder='templates',
static_folder='static')
CORS(self.app)
self._setup_routes()
def _setup_routes(self):
"""设置路由"""
@self.app.route('/')
def index():
return render_template('index.html')
@self.app.route('/api/chat', methods=['POST'])
def chat():
data = request.json
message = data.get('message', '')
# 简单响应,实际应集成完整的聊天逻辑
return jsonify({
'response': f'收到消息: {message}\n\n请使用TUI界面获得完整功能。',
'timestamp': datetime.now().isoformat()
})
@self.app.route('/api/memory/recall', methods=['POST'])
def recall():
data = request.json
result = self.db.recall(
query_intent=data.get('query_intent', ''),
seed_entities=data.get('seed_entities'),
depth=data.get('depth', 2)
)
return jsonify(result)
@self.app.route('/api/memory/commit', methods=['POST'])
def commit():
data = request.json
result = self.db.commit(
triplets=data.get('triplets', []),
entity_types=data.get('entity_types'),
session_id=data.get('session_id'),
turn_id=data.get('turn_id')
)
return jsonify(result)
@self.app.route('/api/memory/introspect', methods=['GET'])
def introspect():
result = self.db.introspect()
return jsonify(result)
@self.app.route('/api/config', methods=['GET'])
def get_config():
return jsonify({
'api_key': self.config.api_key[:10] + '...' if self.config.api_key else '',
'model': self.config.model,
'base_url': self.config.base_url
})
@self.app.route('/api/config', methods=['POST'])
def save_config():
data = request.json
self.config.api_key = data.get('api_key', '')
self.config.model = data.get('model', 'deepseek-chat')
self.config.base_url = data.get('base_url', 'https://api.deepseek.com')
return jsonify({'status': 'ok', 'message': 'Configuration saved'})
def run(self):
"""启动Web服务"""
print(f"Web interface running at http://localhost:{self.port}")
self.app.run(host='0.0.0.0', port=self.port, debug=False, threaded=True)
def run_async(self):
"""异步启动Web服务"""
import threading
thread = threading.Thread(target=self.run, daemon=True)
thread.start()
return thread

View File

@ -1,408 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Graph Memory TUI - Web Interface</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Courier New', monospace;
background: #1a1a1a;
color: #e0e0e0;
height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.header {
background: #2d2d2d;
padding: 10px 20px;
border-bottom: 2px solid #4a9eff;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #4a9eff;
font-size: 18px;
}
.header .status {
color: #888;
font-size: 12px;
}
/* Main Container */
.container {
display: flex;
flex: 1;
overflow: hidden;
}
/* Left Panel - Chat */
.left-panel {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid #333;
}
/* Message History */
.message-history {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #0d0d0d;
}
.message {
margin-bottom: 15px;
padding: 10px;
border-radius: 5px;
}
.message.user {
background: #1e3a5f;
border-left: 3px solid #4a9eff;
}
.message.assistant {
background: #2d2d2d;
border-left: 3px solid #50c878;
}
.message .role {
font-weight: bold;
margin-bottom: 5px;
font-size: 12px;
color: #888;
}
.message .content {
white-space: pre-wrap;
line-height: 1.5;
}
.message .timestamp {
font-size: 10px;
color: #666;
margin-top: 5px;
}
/* Input Box */
.input-box {
padding: 15px;
background: #1a1a1a;
border-top: 1px solid #333;
}
.input-box textarea {
width: 100%;
background: #0d0d0d;
color: #e0e0e0;
border: 1px solid #4a9eff;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 14px;
resize: none;
height: 60px;
}
.input-box textarea:focus {
outline: none;
border-color: #50c878;
}
.input-box .hint {
font-size: 11px;
color: #666;
margin-top: 5px;
}
/* Right Panel - Config & Logs */
.right-panel {
width: 350px;
background: #1a1a1a;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.panel-section {
padding: 15px;
border-bottom: 1px solid #333;
}
.panel-section h3 {
color: #4a9eff;
font-size: 14px;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #333;
}
/* Config */
.config-item {
margin-bottom: 15px;
}
.config-item label {
display: block;
font-size: 12px;
color: #888;
margin-bottom: 5px;
}
.config-item input {
width: 100%;
background: #0d0d0d;
color: #e0e0e0;
border: 1px solid #333;
padding: 8px;
font-family: 'Courier New', monospace;
font-size: 12px;
}
.config-item input:focus {
outline: none;
border-color: #4a9eff;
}
.config-hint {
font-size: 11px;
color: #666;
font-style: italic;
}
/* Operation Log */
.operation-log {
flex: 1;
overflow-y: auto;
}
.log-entry {
padding: 8px;
margin-bottom: 8px;
background: #0d0d0d;
border-left: 2px solid #50c878;
font-size: 11px;
}
.log-entry .time {
color: #666;
}
.log-entry .tool {
color: #4a9eff;
font-weight: bold;
}
/* Buttons */
.btn {
background: #4a9eff;
color: #fff;
border: none;
padding: 8px 15px;
cursor: pointer;
font-family: 'Courier New', monospace;
font-size: 12px;
margin-top: 10px;
}
.btn:hover {
background: #50c878;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #1a1a1a;
}
::-webkit-scrollbar-thumb {
background: #333;
}
::-webkit-scrollbar-thumb:hover {
background: #4a9eff;
}
</style>
</head>
<body>
<div class="header">
<h1>Graph Memory TUI - Web Interface</h1>
<div class="status">Connected | Database: SQLite</div>
</div>
<div class="container">
<!-- Left Panel -->
<div class="left-panel">
<div class="message-history" id="messageHistory">
<div class="message assistant">
<div class="role">Assistant</div>
<div class="content">欢迎使用 Graph Memory TUI Web界面
这是一个让AI拥有长期记忆的图记忆系统。
功能:
• 记忆检索 - 查找历史信息
• 记忆写入 - 保存重要信息
• 记忆管理 - 删除、归档记忆
请先在右侧配置API Key然后开始对话。</div>
<div class="timestamp">System</div>
</div>
</div>
<div class="input-box">
<textarea id="userInput" placeholder="输入消息... (Ctrl+Enter发送)"></textarea>
<div class="hint">按 Ctrl+Enter 发送消息</div>
</div>
</div>
<!-- Right Panel -->
<div class="right-panel">
<div class="panel-section">
<h3>━━ 配置 ━━</h3>
<div class="config-item">
<label>API Key:</label>
<input type="text" id="apiKey" placeholder="sk-xxxxxxxxxxxxx">
</div>
<div class="config-item">
<label>模型:</label>
<input type="text" id="model" value="deepseek-chat">
</div>
<div class="config-item">
<label>Base URL:</label>
<input type="text" id="baseUrl" value="https://api.deepseek.com">
</div>
<button class="btn" onclick="saveConfig()">保存配置</button>
<div class="config-hint">配置会自动保存到本地</div>
</div>
<div class="panel-section">
<h3>━━ 数据库状态 ━━</h3>
<div id="dbStatus">加载中...</div>
<button class="btn" onclick="refreshStatus()">刷新状态</button>
</div>
<div class="panel-section operation-log">
<h3>━━ 操作日志 ━━</h3>
<div id="operationLog"></div>
</div>
</div>
</div>
<script>
// Message handling
const messageHistory = document.getElementById('messageHistory');
const userInput = document.getElementById('userInput');
function addMessage(role, content) {
const msg = document.createElement('div');
msg.className = `message ${role}`;
msg.innerHTML = `
<div class="role">${role === 'user' ? 'You' : 'Assistant'}</div>
<div class="content">${content}</div>
<div class="timestamp">${new Date().toLocaleTimeString()}</div>
`;
messageHistory.appendChild(msg);
messageHistory.scrollTop = messageHistory.scrollHeight;
}
function addLog(tool, result) {
const log = document.getElementById('operationLog');
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.innerHTML = `
<div class="time">${new Date().toLocaleTimeString()}</div>
<div class="tool">${tool}</div>
<div>${result}</div>
`;
log.insertBefore(entry, log.firstChild);
}
// Send message
async function sendMessage() {
const message = userInput.value.trim();
if (!message) return;
addMessage('user', message);
userInput.value = '';
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({message})
});
const data = await response.json();
addMessage('assistant', data.response);
addLog('chat', 'Message sent');
} catch (error) {
addMessage('assistant', `Error: ${error.message}`);
}
}
// Config
async function saveConfig() {
const config = {
api_key: document.getElementById('apiKey').value,
model: document.getElementById('model').value,
base_url: document.getElementById('baseUrl').value
};
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(config)
});
const data = await response.json();
addLog('config', 'Configuration saved');
alert('配置已保存!');
} catch (error) {
alert('保存失败:' + error.message);
}
}
// Status
async function refreshStatus() {
try {
const response = await fetch('/api/memory/introspect');
const data = await response.json();
document.getElementById('dbStatus').innerHTML = `
<div>实体数量: ${data.entity_count}</div>
<div>关系数量: ${data.relation_count}</div>
`;
addLog('introspect', 'Status refreshed');
} catch (error) {
document.getElementById('dbStatus').innerHTML = 'Error: ' + error.message;
}
}
// Keyboard shortcuts
userInput.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'Enter') {
sendMessage();
}
});
// Initialize
refreshStatus();
</script>
</body>
</html>

54
test_message_order.py Normal file
View File

@ -0,0 +1,54 @@
"""测试消息顺序"""
from graph_memory_tui.core.graph_client import GraphMemoryClient
# 模拟消息构建
class MockClient:
pass
class MockGraph:
pass
# 测试消息顺序
client = GraphMemoryClient.__new__(GraphMemoryClient)
client.client = MockClient()
client.graph = MockGraph()
client.tools = []
client.system_prompt = 'You are a helpful assistant.'
# 模拟第一次调用(无工具结果)
print('=== 第一次调用(无工具结果)===')
messages = [{'role': 'system', 'content': client.system_prompt}]
messages.append({'role': 'user', 'content': '你好'})
for i, msg in enumerate(messages):
content = str(msg.get('content', ''))[:50]
print(f'{i+1}. {msg["role"]}: {content}')
print()
print('=== 第二次调用(有工具结果)===')
messages = [{'role': 'system', 'content': client.system_prompt}]
messages.append({'role': 'user', 'content': '你好'})
messages.append({
'role': 'assistant',
'content': None,
'tool_calls': [{'id': 'call_123', 'type': 'function', 'function': {'name': 'memory_recall', 'arguments': '{}'}}]
})
messages.append({'role': 'tool', 'tool_call_id': 'call_123', 'content': '查询结果...'})
for i, msg in enumerate(messages):
role = msg.get('role')
if role == 'tool':
tid = msg.get('tool_call_id')
print(f'{i+1}. {role}: tool_call_id={tid}')
elif role == 'assistant' and msg.get('tool_calls'):
tc_count = len(msg.get('tool_calls'))
print(f'{i+1}. {role}: tool_calls={tc_count}')
else:
content = str(msg.get('content', ''))[:50]
print(f'{i+1}. {role}: {content}')
print()
print('✅ 消息顺序正确!')
print('AI 可以看到:')
print('1. 用户说了什么')
print('2. 自己调用了什么工具')
print('3. 工具返回了什么结果')

52
test_tool_call_flow.py Normal file
View File

@ -0,0 +1,52 @@
"""
测试工具调用的正确流程
OpenAI API 工具调用的正确消息顺序:
第一轮:
1. system
2. user: "帮我查询用户信息"
→ AI 返回 tool_calls: [memory_recall]
第二轮:
1. system
2. user: "帮我查询用户信息"
3. assistant: tool_calls=[memory_recall]
4. tool: "查询结果..."
→ AI 返回 tool_calls: [memory_commit]
第三轮:
1. system
2. user: "帮我查询用户信息"
3. assistant: tool_calls=[memory_recall]
4. tool: "查询结果..."
5. assistant: tool_calls=[memory_commit]
6. tool: "写入结果..."
→ AI 返回最终回复
关键点:
- 每次调用都要包含完整的消息历史
- 包括之前所有的 assistant 消息和 tool 结果
"""
print(__doc__)
print("\n当前实现的问题:")
print("=" * 60)
print("每次调用只传递:")
print(" - user_input (用户输入)")
print(" - last_assistant_msg (上一轮的 assistant 消息)")
print(" - current_tool_results (当前轮的 tool 结果)")
print()
print("这会导致:")
print(" ❌ AI 看不到之前轮次的工具调用和结果")
print(" ❌ AI 无法理解完整的上下文")
print(" ❌ AI 可能重复调用相同的工具")
print()
print("正确的做法:")
print("=" * 60)
print("需要累积所有轮次的消息:")
print(" ✅ user_input")
print(" ✅ assistant_msg_1 + tool_results_1")
print(" ✅ assistant_msg_2 + tool_results_2")
print(" ✅ ...")