feat(api): 访谈人格/回复长度策略、口述归一、背景语气与输入净稿全链路
Chat 访谈 - 新增 persona 系统(default / warm_listener / curious_guide)与 background_voice 语气层 - 回复长度由 compute_reply_plan 统一决策(brief / standard / expanded),融合信息密度启发式 - 输入净稿(input_normalize):编排层可选 rules/llm 归一用户口语后再喂模型与记忆检索 - 记忆证据注入:按用户话检索 memory evidence 并注入 prompt Memoir 回忆录 - 口述归一(oral_normalize):segment 原文保留,story 管线取派生净稿作叙事输入 - segment 入队批次门闸:累计字数 + 最长等待秒数,减少零碎提交 - fidelity_check / prompts / narrative_agent 微调 - Alembic 0005:清理跨章节 story 外键 Infra - Dockerfile 加入 ffmpeg - pyproject.toml 新增依赖并同步 uv.lock - .env.example / .env.production 补全新配置项 Tests - 新增 test_background_voice、test_chat_input_normalize、test_experience_regressions - 扩展 test_interview_prompts、test_interview_reply_length、test_story_route_oral_invariant Made-with: Cursor
This commit is contained in:
@@ -19,8 +19,20 @@ from app.agents.chat.stage_detection import (
|
||||
detect_primary_life_stage,
|
||||
life_stage_display_name,
|
||||
)
|
||||
from app.core.config import settings
|
||||
from app.core.dependencies import get_llm_provider
|
||||
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
|
||||
|
||||
|
||||
def _llm_for_chat_input_normalize():
|
||||
try:
|
||||
p = get_llm_provider()
|
||||
return getattr(p, "langchain_llm", None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.features.user.models import User
|
||||
|
||||
@@ -31,6 +43,41 @@ _UNAUTH_TURN = AgentChatTurn(
|
||||
)
|
||||
|
||||
|
||||
async def _fetch_interview_memory_evidence(
|
||||
db: AsyncSession,
|
||||
user_id: str,
|
||||
user_message: str,
|
||||
) -> str:
|
||||
"""按本轮用户话检索记忆,格式化为短文本;失败或未启用时返回空串。"""
|
||||
from app.core.dependencies import get_embedding_provider
|
||||
from app.features.memory.evidence_format import format_evidence_chunks_for_prompt
|
||||
from app.features.memory.service import MemoryService
|
||||
|
||||
if not settings.chat_memory_retrieval_enabled:
|
||||
return ""
|
||||
msg = (user_message or "").strip()
|
||||
if not msg:
|
||||
return ""
|
||||
try:
|
||||
ms = MemoryService(db, embedding_provider=get_embedding_provider())
|
||||
bundle = await ms.retrieve(user_id, msg, top_k=settings.chat_memory_top_k)
|
||||
text = format_evidence_chunks_for_prompt(bundle.model_dump())
|
||||
t = (text or "").strip()
|
||||
if not t:
|
||||
return ""
|
||||
max_c = settings.chat_memory_evidence_max_chars
|
||||
if len(t) > max_c:
|
||||
return t[: max_c - 3] + "..."
|
||||
return t
|
||||
except Exception as e:
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception as rollback_error:
|
||||
logger.warning("访谈记忆检索失败后回滚也失败: {}", rollback_error)
|
||||
logger.warning("访谈记忆检索失败: {}", e)
|
||||
return ""
|
||||
|
||||
|
||||
class ChatOrchestrator:
|
||||
"""
|
||||
聊天编排器:根据用户资料完成度路由到 ProfileAgent 或 InterviewAgent。
|
||||
@@ -106,6 +153,10 @@ class ChatOrchestrator:
|
||||
return AgentChatTurn(messages=responses, skip_tts=False)
|
||||
except Exception as e:
|
||||
logger.error(f"资料收集处理失败: {e}", exc_info=True)
|
||||
return AgentChatTurn(
|
||||
messages=["不好意思刚才没接住,你再说一遍好吗?"],
|
||||
skip_tts=False,
|
||||
)
|
||||
|
||||
# --- 正式访谈模式 ---
|
||||
user_id = user.id if user else None
|
||||
@@ -125,9 +176,18 @@ class ChatOrchestrator:
|
||||
conversation_id,
|
||||
len(user_message or ""),
|
||||
)
|
||||
llm_n = None
|
||||
if settings.chat_input_normalize_enabled and (
|
||||
(settings.chat_input_normalize_mode or "").strip().lower() == "llm"
|
||||
):
|
||||
llm_n = _llm_for_chat_input_normalize()
|
||||
normalized_user_message = normalize_chat_input_for_agent(
|
||||
user_message or "",
|
||||
llm=llm_n,
|
||||
)
|
||||
state = await get_or_create_state(user_id, db)
|
||||
detected = await detect_primary_life_stage(
|
||||
user_message,
|
||||
normalized_user_message,
|
||||
state.current_stage,
|
||||
self.interview_agent.llm,
|
||||
)
|
||||
@@ -138,9 +198,11 @@ class ChatOrchestrator:
|
||||
conversation.conversation_stage = state.current_stage
|
||||
await db.commit()
|
||||
|
||||
from app.agents.chat.background_voice import infer_background_voice
|
||||
from app.agents.chat.prompts_profile import format_user_profile_context
|
||||
|
||||
user_profile_context = ""
|
||||
background_voice = "default"
|
||||
if user:
|
||||
user_profile_context = format_user_profile_context(
|
||||
birth_year=user.birth_year,
|
||||
@@ -148,6 +210,11 @@ class ChatOrchestrator:
|
||||
grew_up_place=user.grew_up_place,
|
||||
occupation=user.occupation,
|
||||
)
|
||||
background_voice = infer_background_voice(user.occupation)
|
||||
|
||||
memory_evidence_text = await _fetch_interview_memory_evidence(
|
||||
db, user_id, normalized_user_message
|
||||
)
|
||||
|
||||
turn = await self.interview_agent.generate_response_with_state(
|
||||
conversation_id=conversation_id,
|
||||
@@ -155,6 +222,9 @@ class ChatOrchestrator:
|
||||
memoir_state=state,
|
||||
user_profile_context=user_profile_context,
|
||||
detected_user_stage=detected,
|
||||
memory_evidence_text=memory_evidence_text,
|
||||
background_voice=background_voice,
|
||||
normalized_user_message=normalized_user_message,
|
||||
)
|
||||
if agent_summary_enabled():
|
||||
logger.info(
|
||||
@@ -224,6 +294,9 @@ class ChatOrchestrator:
|
||||
user_message_timestamp: datetime | None = None,
|
||||
audio_duration_seconds: int | None = None,
|
||||
detected_user_stage: str | None = None,
|
||||
memory_evidence_text: str = "",
|
||||
background_voice: str = "default",
|
||||
normalized_user_message: str | None = None,
|
||||
) -> AgentChatTurn:
|
||||
"""委托 InterviewAgent 生成访谈回复(持久化由调用方负责)。"""
|
||||
return await self.interview_agent.generate_response_with_state(
|
||||
@@ -232,6 +305,9 @@ class ChatOrchestrator:
|
||||
memoir_state=memoir_state,
|
||||
user_profile_context=user_profile_context,
|
||||
detected_user_stage=detected_user_stage,
|
||||
memory_evidence_text=memory_evidence_text,
|
||||
background_voice=background_voice,
|
||||
normalized_user_message=normalized_user_message,
|
||||
)
|
||||
|
||||
def detect_user_stage(self, user_message: str) -> str:
|
||||
@@ -243,6 +319,7 @@ class ChatOrchestrator:
|
||||
conversation_id: str,
|
||||
memoir_state: MemoirStateSchema,
|
||||
user_profile_context: str = "",
|
||||
background_voice: str = "default",
|
||||
) -> List[str]:
|
||||
"""
|
||||
委托 InterviewAgent 生成访谈开场白(持久化由调用方 ConversationHistoryStore 负责)。
|
||||
@@ -251,4 +328,5 @@ class ChatOrchestrator:
|
||||
conversation_id=conversation_id,
|
||||
memoir_state=memoir_state,
|
||||
user_profile_context=user_profile_context,
|
||||
background_voice=background_voice,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user