feat(api): 收敛对话与记忆流程边界,引入 LLM 网关与专用服务

- MemoryService 异步路径委托 MemoryIngestService / MemoryRetrievalService;富化派发经 MemoryEnrichmentScheduler
- WebSocket pipeline 经 ChatTurnService 与显式 DTO 编排单轮对话;回忆录片段入队由 MemoirIngestScheduler 封装
- 新增 LlmGateway(LlmUseCase),各 agent、任务与适配器对齐 ports
- 补充 memory 提示适配、runtime 类型、memory-retrieval 文档、ai-touchpoints 说明与扫描脚本及配套测试

Made-with: Cursor
This commit is contained in:
Kevin
2026-04-30 09:17:01 +08:00
parent eddb2c3078
commit ac436b87a2
37 changed files with 1400 additions and 199 deletions

View File

@@ -4,6 +4,7 @@ ChatOrchestratorAI 回复用户模块的编排层
"""
import time
from collections.abc import Callable
from datetime import datetime
from typing import TYPE_CHECKING, List, Optional
@@ -24,7 +25,8 @@ from app.agents.chat.stage_detection import (
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.dependencies import get_embedding_provider
from app.core.llm_gateway import LlmGateway
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 (
@@ -32,18 +34,20 @@ from app.features.memoir.state_service import (
save_interview_state_meta,
switch_stage,
)
from app.features.memory.prompt_adapter import MemoryPromptAdapter
def _llm_for_chat_input_normalize():
try:
p = get_llm_provider()
return getattr(p, "langchain_llm", None)
return LlmGateway().langchain_llm_for()
except Exception:
return None
if TYPE_CHECKING:
from app.features.user.models import User
from app.ports.embedding import EmbeddingProvider
from app.ports.llm import LLMProvider
logger = get_logger(__name__)
@@ -56,9 +60,10 @@ async def _fetch_interview_memory_bundle(
db: AsyncSession,
user_id: str,
user_message: str,
*,
get_embedding_provider_fn: Callable[[], "EmbeddingProvider"],
) -> tuple[dict | None, object | None]:
"""检索记忆 bundle原始结构是否进主 prompt 由 `slice_interview_memory` 再筛。"""
from app.core.dependencies import get_embedding_provider
"""检索记忆 bundle原始结构是否进主 prompt 由 adapter 再筛。"""
from app.features.memory.retrieval_trace import (
chat_memory_retrieval_trace_from_bundle,
)
@@ -76,7 +81,7 @@ async def _fetch_interview_memory_bundle(
)
return None, None
try:
emb = get_embedding_provider()
emb = get_embedding_provider_fn()
ms = MemoryService(db, embedding_provider=emb)
top_k = settings.chat_memory_top_k
bundle = await ms.retrieve(user_id, msg, top_k=top_k)
@@ -103,11 +108,22 @@ class ChatOrchestrator:
"""
聊天编排器:根据用户资料完成度路由到 ProfileAgent 或 InterviewAgent。
不直接写入 Redis/DB由 WS pipeline / ConversationHistoryStore 落库并同步缓存。
``get_embedding_provider_fn`` / ``llm_provider`` 供测试或脚本注入;默认使用全局依赖。
"""
def __init__(self):
self.profile_agent = ProfileAgent()
def __init__(
self,
*,
get_embedding_provider_fn: Callable[[], "EmbeddingProvider"] | None = None,
llm_provider: "LLMProvider | None" = None,
):
self._get_embedding_provider_fn = (
get_embedding_provider_fn or get_embedding_provider
)
self.profile_agent = ProfileAgent(llm_provider=llm_provider)
self.interview_agent = InterviewAgent()
self.memory_prompt_adapter = MemoryPromptAdapter()
async def process_user_message(
self,
@@ -272,12 +288,16 @@ class ChatOrchestrator:
background_voice = infer_background_voice(user.occupation)
occupation = user.occupation or ""
from app.features.memory.chat_memory_injection import slice_interview_memory
memory_bundle, mem_trace = await _fetch_interview_memory_bundle(
db, user_id, normalized_user_message
db,
user_id,
normalized_user_message,
get_embedding_provider_fn=self._get_embedding_provider_fn,
)
mem_slices = self.memory_prompt_adapter.slice_for_interview(
memory_bundle,
normalized_user_message,
)
mem_slices = slice_interview_memory(memory_bundle, normalized_user_message)
# 场景关键词仅作为 focus planner 的辅助输入,不直接拼进记忆块,避免抢过用户明确的关系/身份线索
scene_cues_for_planner = extract_scene_cues(normalized_user_message)