feat(api+app): 对话阶段化、回忆录流水线与客户端会话体验

- DB: segments 用户输入文本(Alembic 0002)
- Chat: 阶段检测/阶段提示/回复限制,编排与访谈/画像 prompts 调整
- Memoir: 忠实度检查 agent,叙事与分类等链路更新
- Core: agent 日志、Alembic 启动、LangChain/日志/配置等
- Story: time_hints;Memory 检索与相关测试
- Expo: 助手头像、会话页与消息拆分、实时会话与文案/i18n
- Docs/scripts/tests: 迁移脚本、LLM JSON/记忆检索文档、新增单测
This commit is contained in:
Kevin
2026-03-26 12:13:36 +08:00
parent 49b089354c
commit a3f61fcc0f
94 changed files with 3332 additions and 672 deletions

View File

@@ -6,6 +6,7 @@ MemoirOrchestrator按 segment 编排流水线,调用各 Specialist Agent。
from __future__ import annotations
import time
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Set, Tuple
@@ -17,6 +18,7 @@ from app.agents.memoir.classification_agent import (
)
from app.agents.memoir.extraction_agent import ExtractionAgent, ExtractionResult
from app.agents.state_schema import MemoirStateSchema
from app.core.agent_logging import agent_span, agent_summary_enabled, log_agent_detail
from app.core.logging import get_logger
from app.features.conversation.models import Segment
@@ -58,32 +60,59 @@ class MemoirOrchestrator:
category_to_segments: Dict[str, List[Segment]] = {}
for segment in segments:
text = segment.transcript_text or ""
text = segment.user_input_text or ""
seg_t0 = time.perf_counter()
initial_stage = detect_stage_from_keywords(
text, state.current_stage or "childhood"
)
stage_slots_raw = state.slots.get(initial_stage, {}) or {}
result: ExtractionResult = self.extraction_agent.extract(
user_message=text,
current_stage=state.current_stage or "childhood",
stage_slots=stage_slots_raw,
llm=llm,
)
with agent_span(
logger,
"MemoirOrchestrator.ExtractionAgent.extract",
segment_id=segment.id,
):
result: ExtractionResult = self.extraction_agent.extract(
user_message=text,
current_stage=state.current_stage or "childhood",
stage_slots=stage_slots_raw,
llm=llm,
)
detected_stage = result.detected_stage
for slot_name, snippet in result.slots.items():
state = update_slot(detected_stage, slot_name, snippet, [segment.id])
chapter_category = self.classification_agent.classify(
text=text,
fallback_stage=detected_stage,
llm=llm,
with agent_span(
logger,
"MemoirOrchestrator.ClassificationAgent.classify",
segment_id=segment.id,
):
chapter_category = self.classification_agent.classify(
text=text,
fallback_stage=detected_stage,
llm=llm,
)
if agent_summary_enabled():
logger.info(
"MemoirOrchestrator.segment segment_id={} text_len={} "
"detected_stage={} category={} segment_total_ms={:.2f}",
segment.id,
len(text),
detected_stage,
chapter_category,
(time.perf_counter() - seg_t0) * 1000,
)
log_agent_detail(
logger,
"MemoirOrchestrator.segment_done segment_id={} slots={}",
segment.id,
list((result.slots or {}).keys()),
)
if chapter_category is None:
logger.debug(
"段落无回忆录价值,跳过: segment_id=%s transcript=%s",
"段落无回忆录价值,跳过: segment_id={} transcript={}",
segment.id,
getattr(segment, "transcript_text", None) or "",
getattr(segment, "user_input_text", None) or "",
)
continue
category_to_segments.setdefault(chapter_category, []).append(segment)
@@ -138,7 +167,7 @@ class MemoirOrchestrator:
for chapter_category, category_segments in category_to_segments.items():
if not acquire_lock(chapter_category):
logger.warning(
"章节锁竞争: category=%s, 延迟重试",
"章节锁竞争: category={}, 延迟重试",
chapter_category,
)
raise_retry()