feat: 回忆录证据血缘与内部评测可追溯,顺带对齐本地评测台与 CI

数据库与模型:新增多版迁移(章节证据快照、对话血缘、记忆事实/时间线 lineage 等),把「成稿 ↔ 对话/记忆」的溯源信息落到表结构里。
业务链路:会话与 WS、回忆录/故事流水线、记忆写入与 enrichment 等跟着接上线索与快照;新增章节证据快照与评测侧 EvalTraceService 等模块,方便组评审用的证据包。
内部评测:自动化 run 与手工 memoir 评审共用可追溯证据;rubric/ judge 相关脚本与文档有配套调整。
app-eval-web:Memoir/实验详情里能展开看证据摘要与 evidence_trace(含对话轮次 id);Vite 代理与 development.sh 注入的 API 端口与当前默认内部评测端口一致,避免改端口后页面连错服务。
工程杂项:GitHub Actions / 仓库说明有更新;各适配器与支付/配额/plan 等多处为小改动或跟随主改动的收尾;新增/扩充了?
This commit is contained in:
Kevin
2026-04-08 15:37:09 +08:00
parent 6772e1269c
commit 309a051038
109 changed files with 4125 additions and 858 deletions

View File

@@ -13,15 +13,15 @@ from app.agents.chat.agent_turn import AgentChatTurn
from app.agents.chat.helpers import get_history_with_window
from app.agents.chat.interview_agent import InterviewAgent
from app.agents.chat.profile_agent import ProfileAgent
from app.agents.state_schema import MemoirStateSchema
from app.core.agent_logging import agent_summary_enabled, log_agent_detail
from app.core.logging import get_logger
from app.agents.chat.stage_detection import (
detect_primary_life_stage,
life_stage_display_name,
)
from app.agents.state_schema import MemoirStateSchema
from app.core.agent_logging import agent_summary_enabled, log_agent_detail
from app.core.config import settings
from app.core.dependencies import get_llm_provider
from app.core.logging import get_logger
from app.features.conversation.input_normalize import normalize_chat_input_for_agent
from app.features.memoir.state_service import get_or_create_state, switch_stage
@@ -48,28 +48,35 @@ async def _fetch_interview_memory_evidence(
db: AsyncSession,
user_id: str,
user_message: str,
) -> str:
"""按本轮用户话检索记忆格式化短文本;失败或未启用时返回空串"""
) -> tuple[str, dict | None]:
"""按本轮用户话检索记忆格式化短文本 + 可入库 trace稳定 id"""
from app.core.dependencies import get_embedding_provider
from app.features.memory.evidence_format import format_evidence_chunks_for_prompt
from app.features.memory.retrieval_trace import (
chat_memory_retrieval_trace_from_bundle,
)
from app.features.memory.service import MemoryService
if not settings.chat_memory_retrieval_enabled:
logger.debug(
"event=chat_memory_retrieval_skip reason=disabled user_id={}", user_id
)
return ""
return "", None
msg = (user_message or "").strip()
if not msg:
logger.debug(
"event=chat_memory_retrieval_skip reason=empty user_id={}", user_id
)
return ""
return "", None
try:
emb = get_embedding_provider()
ms = MemoryService(db, embedding_provider=emb)
bundle = await ms.retrieve(user_id, msg, top_k=settings.chat_memory_top_k)
top_k = settings.chat_memory_top_k
bundle = await ms.retrieve(user_id, msg, top_k=top_k)
bd = bundle.model_dump()
trace = chat_memory_retrieval_trace_from_bundle(
bd, top_k=top_k, query_len=len(msg)
)
text = format_evidence_chunks_for_prompt(bd)
t = (text or "").strip()
if not t:
@@ -77,7 +84,7 @@ async def _fetch_interview_memory_evidence(
"event=memory_evidence_for_prompt user_id={} formatted_chars=0",
user_id,
)
return ""
return "", trace
max_c = settings.chat_memory_evidence_max_chars
if len(t) > max_c:
t = t[: max_c - 3] + "..."
@@ -86,14 +93,14 @@ async def _fetch_interview_memory_evidence(
user_id,
len(t),
)
return t
return t, trace
except Exception as e:
try:
await db.rollback()
except Exception as rollback_error:
logger.warning("访谈记忆检索失败后回滚也失败: {}", rollback_error)
logger.warning("访谈记忆检索失败: {}", e)
return ""
return "", None
class ChatOrchestrator:
@@ -197,12 +204,15 @@ class ChatOrchestrator:
conversation_id,
len(responses),
)
return AgentChatTurn(messages=responses, skip_tts=False)
return AgentChatTurn(
messages=responses, skip_tts=False, memory_retrieval_trace=None
)
except Exception as e:
logger.error(f"资料收集处理失败: {e}", exc_info=True)
return AgentChatTurn(
messages=["不好意思刚才没接住,你再说一遍好吗?"],
skip_tts=False,
memory_retrieval_trace=None,
)
# --- 正式访谈模式 ---
@@ -262,10 +272,17 @@ class ChatOrchestrator:
background_voice = infer_background_voice(user.occupation)
occupation = user.occupation or ""
memory_evidence_text = await _fetch_interview_memory_evidence(
memory_evidence_text, mem_trace = await _fetch_interview_memory_evidence(
db, user_id, normalized_user_message
)
profile_birth_year = user.birth_year if user else None
profile_era_place = ""
if user:
profile_era_place = (
(user.birth_place or user.grew_up_place or "").strip()
)
turn = await self.interview_agent.generate_response_with_state(
conversation_id=conversation_id,
user_message=user_message,
@@ -276,6 +293,8 @@ class ChatOrchestrator:
background_voice=background_voice,
normalized_user_message=normalized_user_message,
occupation=occupation,
profile_birth_year=profile_birth_year,
profile_era_place=profile_era_place,
)
if agent_summary_enabled():
logger.info(
@@ -287,6 +306,12 @@ class ChatOrchestrator:
len(turn.messages),
turn.skip_tts,
)
if mem_trace is not None:
return AgentChatTurn(
messages=turn.messages,
skip_tts=turn.skip_tts,
memory_retrieval_trace=mem_trace,
)
return turn
async def extract_profile_from_message(
@@ -349,6 +374,8 @@ class ChatOrchestrator:
background_voice: str = "default",
normalized_user_message: str | None = None,
occupation: str = "",
profile_birth_year: int | None = None,
profile_era_place: str = "",
) -> AgentChatTurn:
"""委托 InterviewAgent 生成访谈回复(持久化由调用方负责)。"""
return await self.interview_agent.generate_response_with_state(
@@ -361,6 +388,8 @@ class ChatOrchestrator:
background_voice=background_voice,
normalized_user_message=normalized_user_message,
occupation=occupation,
profile_birth_year=profile_birth_year,
profile_era_place=profile_era_place,
)
def detect_user_stage(self, user_message: str) -> str: