Skip to content

Commit 80f4851

Browse files
committed
feat: enhance long-term memory extraction with flexible time settings
--bug=1069014@tapd-62980211 --user=刘瑞斌 【长期记忆】历史版本的智能体开启长期记忆后,使用历史对话生成了记忆 https://www.tapd.cn/62980211/s/1916817
1 parent c7b7016 commit 80f4851

1 file changed

Lines changed: 131 additions & 50 deletions

File tree

apps/application/long_term_memory/__init__.py

Lines changed: 131 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import re
2+
from datetime import timedelta
23

34
import uuid_utils.compat as uuid
45
from django.db.models import Count, QuerySet
6+
from django.utils import timezone
57
from langchain_core.messages import HumanMessage
68

79
from application.models import Chat, ChatRecord, Application, ApplicationLongTermMemory
@@ -10,79 +12,111 @@
1012
from ops import celery_app
1113

1214
long_term_prompt = '''
13-
你是一个专业的长期记忆管理助手,负责从对话中提炼并维护用户的结构化长期记忆
15+
你是一个专业的用户长期记忆提炼引擎。你的唯一职责是:从对话中精确识别具有持久价值的用户信息,并与已有记忆进行结构化融合,输出供 AI 助手长期使用的用户画像记忆
1416
1517
## 输入
16-
1718
【已有记忆】:
1819
{{existing_memory}}
1920
20-
新对话内容】:
21+
本轮新增对话】:
2122
{{new_conversation}}
2223
2324
---
2425
25-
## 任务说明
26+
## 提取门槛(必须同时满足,才可提取)
27+
28+
1. **跨会话复用价值**:这条信息在未来其他对话中仍然适用,而非当次临时需求
29+
2. **明确可证**:可从对话原文直接支撑,不得推断、脑补或延伸
30+
3. **改善回答质量**:记住这条信息后,AI 的回答会对该用户更准确或更贴合
31+
32+
**以下内容禁止提取:**
33+
- 用户的一次性临时要求(如「这次用表格输出就好」)
34+
- 用户提问的具体内容本身(问题不是记忆)
35+
- 无法从对话原文直接证明的推断
36+
- 闲聊、问候、感谢等无信息量内容
37+
- AI 的回答内容(只提取用户侧信息)
38+
39+
---
40+
41+
## 四类记忆分类与融合规则
42+
43+
### 【偏好】交互偏好
44+
用户对「AI 如何回应」的稳定期望,需明确声明或在多轮中反复体现才可录入。
2645
27-
根据以上输入,生成更新后的长期记忆。遵循以下逻辑:
28-
- 若【已有记忆】为空:仅从【新对话内容】中提炼结构化记忆。
29-
- 若【已有记忆】不为空:在其基础上进行增量融合——新信息覆盖或补充旧信息,不得删除未被新对话否定的已有记忆。
46+
常见维度:回答详略 / 语言风格(正式/口语)/ 输出格式(表格/列表/段落)/ 是否要举例 / 代码风格偏好 / 回复语言
47+
48+
融合规则:
49+
- 同维度出现新偏好 → **覆盖**旧值,条目末标注 `※已更新`
50+
- 新维度 → 直接追加
51+
- 旧偏好无新证据但未被否定 → **保留**
3052
3153
---
3254
33-
## 处理规则
55+
### 【背景】用户背景
56+
用户的客观身份与环境信息,稳定性强,用户未明确更正则不主动变动。
3457
35-
严格按以下三类处理,**不得推测、捏造或补全对话中未明确出现的信息**:
58+
常见维度:职业/角色 / 所在行业 / 技术栈与熟练度 / 使用产品或系统 / 团队规模 / 所在地区
3659
37-
### 一、用户偏好
38-
> 关注用户对"如何回答"的期望与习惯
60+
融合规则:
61+
- 与旧记忆冲突 → **以新对话为准**,标注 `※已更新`,删除旧值
62+
- 新增信息 → 追加
63+
- 信息模糊无法确认 → 追加时标注 `※待确认`
3964
40-
常见维度:回答风格、回答长度、语言风格、格式偏好、编程语言、是否需要举例、是否需要解释、输出语言等
65+
---
4166
42-
- 已有记忆为空:从新对话中提取,无则写「无」
43-
- 已有记忆不为空:新偏好**覆盖**同维度旧偏好;新维度**追加**
67+
### 【约定】明确约定
68+
用户明确要求 AI 固定遵守的行为规则,须有明确指令性语言支撑,不可自行解读。
4469
45-
### 二、关键事实
46-
> 关注用户客观背景信息
70+
常见维度:禁止行为 / 固定执行动作 / 特定触发词响应 / 内容边界 / 输出限制
4771
48-
常见维度:职业、行业、技术栈、身份、使用场景、设备环境、地域、项目背景、当前需求等
72+
融合规则:
73+
- 同类新规则 → **覆盖**旧规则,标注 `※已更新`
74+
- 新增规则 → 追加
75+
- 用户明确取消的规则 → **直接删除**
4976
50-
- 已有记忆为空:从新对话中提取,无则写「无」
51-
- 已有记忆不为空:新对话中与旧记忆**冲突的事实以新对话为准**;新事实**追加**
77+
---
5278
53-
### 三、规则约定
54-
> 关注用户明确提出的行为约束或指令规则
79+
### 【目标】当前目标
80+
用户近期或长期正在推进的具体目标,有助于 AI 主动提供更相关的帮助。
5581
56-
常见维度:触发词、执行动作、禁止动作、生效条件、生效时间范围等
82+
常见维度:正在进行的项目 / 学习计划 / 待解决的核心问题 / 关键决策
5783
58-
- 已有记忆为空:从新对话中提取,无则写「无」
59-
- 已有记忆不为空:新规则**覆盖**同类旧规则;新规则**追加**
84+
融合规则:
85+
- 已明确完成或放弃的目标 → **删除**
86+
- 新目标 → 追加
87+
- 已有目标有进展更新 → **覆盖**旧描述
6088
6189
---
6290
63-
## 输出要求
91+
## 输出规范
6492
65-
1. **只输出结构化记忆本身**,不得包含任何开场白、解释、总结或额外说明
66-
2. 每条记忆使用 `- [维度标签]` 开头,标签尽量精准简洁
67-
3. 某类确实无内容时,必须明确写「无」,不得省略该章节
68-
4. 输出语言与【新对话内容】保持一致
93+
1. **只输出记忆内容本身**,不含任何开头语、解释、总结或分隔说明
94+
2. 四个章节**全部输出**,确无内容写「暂无」,不可省略章节
95+
3. 每条格式:`- [维度标签] 内容`,标签 2~5 字,精准简洁
96+
4. 有变更标记(`※已更新` / `※待确认`)的条目置于各章节**最前**
97+
5. 每条记忆控制在 **60 字以内**,信息密度优先,超出则拆为两条
98+
6. 输出语言与【本轮新增对话】主要语言保持一致
99+
100+
---
69101
70102
## 输出格式
71103
72-
### 一、用户偏好
73-
- [维度标签] 具体内容
74-
- [维度标签] 具体内容
75-
(若无则写:无)
104+
### 【偏好】交互偏好
105+
- [维度标签] 内容
106+
(暂无则写:暂无)
107+
108+
### 【背景】用户背景
109+
- [维度标签] 内容
110+
(暂无则写:暂无)
111+
112+
### 【约定】明确约定
113+
- [维度标签] 内容
114+
(暂无则写:暂无)
76115
77-
### 二、关键事实
78-
- [维度标签] 具体内容
79-
- [维度标签] 具体内容
80-
(若无则写:无)
116+
### 【目标】当前目标
117+
- [维度标签] 内容
118+
(暂无则写:暂无)
81119
82-
### 三、规则约定
83-
- [维度标签] 具体内容
84-
- [维度标签] 具体内容
85-
(若无则写:无)
86120
'''
87121

88122

@@ -121,20 +155,64 @@ def _get_long_term_config(application, chat_user_id):
121155
}
122156

123157

124-
def _run_extract(workspace_id, application_id, chat_user_id, config, history_limit):
158+
def _get_since_time_from_setting(setting: dict):
125159
"""
126-
执行一次长期记忆提取:取最近 history_limit 条对话,调用模型生成/更新记忆。
160+
根据定时设置推算本次应提取的对话起始时间。
161+
返回 datetime(aware),或 None 表示无法推断(回退到 rounds 限制)。
127162
"""
128-
if history_limit <= 0:
163+
now = timezone.now()
164+
schedule_type = setting.get("schedule_type")
165+
166+
if schedule_type == "daily":
167+
return now - timedelta(days=1)
168+
if schedule_type == "weekly":
169+
return now - timedelta(weeks=1)
170+
if schedule_type == "monthly":
171+
return now - timedelta(days=30)
172+
if schedule_type == "interval":
173+
unit = (setting.get("interval_unit") or "").strip()
174+
try:
175+
value_i = int(setting.get("interval_value"))
176+
if value_i <= 0:
177+
return None
178+
except Exception:
179+
return None
180+
delta_map = {
181+
"seconds": timedelta(seconds=value_i),
182+
"minutes": timedelta(minutes=value_i),
183+
"hours": timedelta(hours=value_i),
184+
"days": timedelta(days=value_i),
185+
}
186+
delta = delta_map.get(unit)
187+
return now - delta if delta else None
188+
# cron 等无法从表达式推断间隔,返回 None
189+
return None
190+
191+
192+
def _run_extract(workspace_id, application_id, chat_user_id, config, history_limit=None, since_time=None):
193+
"""
194+
执行一次长期记忆提取。
195+
- since_time 不为 None 时:提取该时间点之后产生的对话。
196+
- 否则按 history_limit 条数限制。
197+
"""
198+
if since_time is None and (history_limit is None or history_limit <= 0):
129199
return
130200

131-
history_chat_record = list(
132-
QuerySet(ChatRecord).filter(
201+
qs = (
202+
QuerySet(ChatRecord)
203+
.filter(
133204
chat__application_id=application_id,
134205
chat__chat_user_id=chat_user_id,
135-
).order_by('-create_time').only('problem_text', 'answer_text')[:history_limit]
206+
)
207+
.order_by('-create_time')
208+
.only('problem_text', 'answer_text')
136209
)
137-
if len(history_chat_record) <= 1:
210+
211+
if since_time is not None:
212+
history_chat_record = list(qs.filter(create_time__gte=since_time))
213+
else:
214+
history_chat_record = list(qs[:history_limit])
215+
if len(history_chat_record) == 0:
138216
return
139217

140218
chat_model = get_model_instance_by_model_workspace_id(
@@ -238,9 +316,12 @@ def _execute_scheduled_extract(workspace_id, application_id):
238316
continue
239317
if config['trigger_type'] != 'SCHEDULED':
240318
continue
241-
history_limit = (config['trigger_setting'] or {}).get('rounds', 20)
319+
setting = config['trigger_setting'] or {}
320+
since_time = _get_since_time_from_setting(setting)
321+
history_limit = None if since_time is not None else setting.get('rounds', 20)
242322
try:
243-
_run_extract(workspace_id, application_id, chat_user_id, config, history_limit=history_limit)
323+
_run_extract(workspace_id, application_id, chat_user_id, config,
324+
history_limit=history_limit, since_time=since_time)
244325
except Exception as e:
245326
maxkb_logger.warning(
246327
f"scheduled extract long_term_memory failed, "

0 commit comments

Comments
 (0)