记忆系统:recall 结果接入 prompt + 可观测性(既有改动,代为提交)
## 说明
**这批改动不是本次会话写的**,它们在会话开始前就已在工作区里、一直未提交。
我做的是**验证**它确实成立,然后按你的指示代为提交。
出处:`docs/演示用/记忆系统排查报告-2026-09-14.md` 与同目录
`记忆系统修复文档-2026-09-14.md`(两份都在本次一并入库)。
排查报告的结论是「记忆系统没有坏」——库里有真实数据、170 条抽取事件全部消费成功;
真正的问题是「观测不到」+「召回结果没人消费」。
## 改动内容(按两份文档的编号)
- **F1 `RecalledMemory.content` 断头路**:`base.py` 新增 `memory_context_text()`,
`risk_agent._agent_system_prompt` 接收并注入记忆段。无记忆时返回空串,
因此 prompt 逐字不变 —— 这也是它能安全接线的理由。
- **F3 `governance.recall` 员工身份恒空**:补一条明确的语义日志。
员工身份下召回的是"该用户自身作为客户"的记忆,恒为空属预期,
但此前没有任何提示,运维看到 `count=0` 只会以为记忆坏了。
- **F4 可观测性**:`GET /api/v1/users/me/memories`(`stored` / `recalled` /
`downstream` / `pending_events` 四段)+ 抽取与召回的 6 处日志 +
三个只读探针 `tools/probe_memory_state.py`、`probe_memory_detail.py`、
`probe_agent_types.py`。
**未实施**(文档明确留作待决,我也不代为决定):F2 `known` 引用校验永不触发
(需架构确认 memory 类 `source_references` 由业务填还是底座统一附加)、
F5 客服是否读写长期记忆(涉脱敏与复核,需产品+合规)。
## 我做的验证(会话内实测,非照录文档)
- 新接口 `GET /users/me/memories` 以 `cust_t` 调用 -> **HTTP 200**:
stored: total=2, by_status={'active': 2}
recalled: count=2, degraded=False
两条记忆:preference:horizon='约三年'(0.95)、preference:risk_level='稳健型'(0.98)
与排查报告 §〇 列出的那两条**完全吻合**。
- `pytest tests/unit tests/contract` 全绿(这批改动没有破坏既有测试)。
## 未验证的部分
`memory_context_text()` 接进 prompt 后的**端到端效果没有实测** —— 文档自己说明了
原因:当前 `risk` Agent 的召回恒空(员工身份不是客户),所以接线后行为不变,
要用测试替身才能验证注入。我没有为此编造证据。
This commit is contained in:
@@ -68,6 +68,8 @@ class MemoryExtractionWorker:
|
||||
result_message_id = int(payload["message_id"])
|
||||
customer_id = int(payload["customer_id"])
|
||||
run_id = str(payload.get("run_id", ""))
|
||||
logger.info("memory extraction start run_id=%s customer_id=%s message_id=%s",
|
||||
run_id, customer_id, result_message_id)
|
||||
result_message = await self.session.get(ConversationMessage, result_message_id)
|
||||
if result_message is None:
|
||||
logger.warning("memory extraction skipped: message missing message_id=%s",
|
||||
@@ -92,8 +94,14 @@ class MemoryExtractionWorker:
|
||||
extracted = await self.extractor.extract(message=source_text)
|
||||
if extracted is None:
|
||||
# 模型判定这条消息没有持久事实:不是错误,但也没有可写的记忆。
|
||||
logger.info("memory extraction found no durable fact run_id=%s", run_id)
|
||||
logger.info("memory extraction found no durable fact run_id=%s source=%r",
|
||||
run_id, source_text[:120])
|
||||
return False
|
||||
logger.info(
|
||||
"memory extraction extracted run_id=%s key=%s type=%s confidence=%s value=%r",
|
||||
run_id, extracted.memory_key, extracted.memory_type, extracted.confidence,
|
||||
extracted.value,
|
||||
)
|
||||
service = MemoryService(self.session, cache=self.cache)
|
||||
memory = await service.upsert(
|
||||
customer_id,
|
||||
@@ -111,6 +119,12 @@ class MemoryExtractionWorker:
|
||||
"session_id": result_message.session_id,
|
||||
},
|
||||
)
|
||||
logger.info(
|
||||
"memory upsert done run_id=%s customer_id=%s memory_uuid=%s key=%s status=%s "
|
||||
"confidence=%s",
|
||||
run_id, customer_id, memory.memory_uuid, memory.memory_key, memory.status,
|
||||
memory.confidence,
|
||||
)
|
||||
recorded = await service.record_evidence(
|
||||
memory,
|
||||
idempotency_key=idempotency_key,
|
||||
|
||||
+22
-5
@@ -191,15 +191,32 @@ class WorkerRuntime:
|
||||
result: AgentResult, business_events: tuple[str, ...] | list[str],
|
||||
) -> bool:
|
||||
"""长期记忆抽取只接收非客服、非访客的明确业务事实。"""
|
||||
if agent_type == "customer_service" or "visitor" in context.roles:
|
||||
if agent_type == "customer_service":
|
||||
# 客服走的是「候选画像」链路(customer_profile.candidate_requested),
|
||||
# 不写长期记忆。此前这里静默 return False,运维无法区分"不该抽"与"该抽但没抽"。
|
||||
logger.info("memory extraction skipped: agent_type=%s 走候选画像链路,不写长期记忆",
|
||||
agent_type)
|
||||
return False
|
||||
return MemoryService.should_extract_memory(
|
||||
if "visitor" in context.roles:
|
||||
logger.info("memory extraction skipped: 访客身份不写长期记忆")
|
||||
return False
|
||||
tool_result = any(call.status == "succeeded" for call in result.result.tool_calls)
|
||||
signals = MemoryService.detect_memory_signals(message)
|
||||
event_type = business_events[0] if business_events else None
|
||||
decision = MemoryService.should_extract_memory(
|
||||
conversation_content=message,
|
||||
role="user",
|
||||
tool_result=any(call.status == "succeeded" for call in result.result.tool_calls),
|
||||
event_type=business_events[0] if business_events else None,
|
||||
signals=MemoryService.detect_memory_signals(message),
|
||||
tool_result=tool_result,
|
||||
event_type=event_type,
|
||||
signals=signals,
|
||||
)
|
||||
logger.info(
|
||||
"memory extraction decision=%s agent_type=%s tool_result=%s event_type=%s "
|
||||
"signals=%s message_len=%s preview=%r",
|
||||
decision, agent_type, tool_result, event_type, list(signals), len(message),
|
||||
message[:80],
|
||||
)
|
||||
return decision
|
||||
|
||||
@staticmethod
|
||||
def should_request_profile_candidate(
|
||||
|
||||
Reference in New Issue
Block a user