For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 用 ChatAgent 替代 Dify 核心聊天能力,接入三层记忆系统,RAG Pipeline 升级为混合检索+重排序。
Architecture: LangGraph StateGraph 编排聊天流程(意图分类→上下文加载→LLM+Tool→任务提取),LangChain Memory 管理三层记忆,RAG 升级为 EnsembleRetriever + ContextualCompressionRetriever。
Tech Stack: Python 3.11, FastAPI, LangChain, LangGraph, ChromaDB, httpx
D:\workspace\cfc\Files:
cfc-langgraph/app/memory/__init__.pycfc-langgraph/app/memory/store.pyInterfaces:
Produces: MemoryManager 类,提供 load(user_id, conv_id) / save(user_id, conv_id, messages) / recall(user_id, query)
[x] Step 1: 创建 app/memory/__init__.py
from .store import MemoryManager
[x] Step 2: 创建 app/memory/store.py
from langchain.memory import ConversationSummaryBufferMemory, VectorStoreRetrieverMemory
from langchain.memory import ConversationBufferWindowMemory
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from app.config import settings
from typing import Optional
import logging
logger = logging.getLogger(__name__)
class MemoryManager:
"""三层记忆管理器
Layer 1 - 工作记忆: 最近 20 轮 + 超出自动摘要
Layer 2 - 长期事实: VectorStoreRetrieverMemory, 跨会话相似召回
Layer 3 - 语义记忆: 每次对话后向量化存储, 供后续会话召回
"""
def __init__(self):
self.llm = ChatOpenAI(
model=settings.llm_model,
api_key=settings.llm_api_key,
base_url=settings.llm_base_url,
)
self.embeddings = OpenAIEmbeddings(
model=settings.embedding_model,
api_key=settings.effective_embedding_api_key,
base_url=settings.effective_embedding_base_url,
)
# Layer 3 向量库: 历史对话记忆
self.memory_vectorstore = Chroma(
collection_name="user_memory",
embedding_function=self.embeddings,
persist_directory=settings.chroma_db_path + "_memory",
)
def get_working_memory(self) -> ConversationSummaryBufferMemory:
"""Layer 1: 工作记忆 (当前会话)"""
return ConversationSummaryBufferMemory(
llm=self.llm,
max_token_limit=2000,
memory_key="history",
return_messages=True,
)
def get_longterm_memory(self, user_id: int) -> VectorStoreRetrieverMemory:
"""Layer 2+3: 长期 + 语义记忆"""
return VectorStoreRetrieverMemory(
retriever=self.memory_vectorstore.as_retriever(
search_kwargs={
"k": 3,
"filter": {"user_id": user_id},
}
),
memory_key="long_term_memory",
input_key="input",
)
async def save_conversation(self, user_id: int, conversation_id: str,
messages: list[dict]):
"""会话结束后保存到向量记忆库"""
texts = []
for msg in messages:
role = msg.get("role", "unknown")
content = msg.get("content", "")
texts.append(f"[{role}] {content}")
full_text = "\n".join(texts)
metadata = {
"user_id": user_id,
"conversation_id": conversation_id,
"timestamp": str(__import__("datetime").datetime.now()),
}
await self.memory_vectorstore.aadd_texts(
texts=[full_text],
metadatas=[metadata],
)
self.memory_vectorstore.persist()
logger.info("已保存对话到向量记忆: conv=%s, user=%s", conversation_id, user_id)
async def recall(self, user_id: int, query: str, k: int = 3) -> list[str]:
"""语义召回: 查询与 query 最相似的历史对话片段"""
results = self.memory_vectorstore.similarity_search(
query,
k=k,
filter={"user_id": user_id},
)
return [doc.page_content for doc in results]
[x] Step 3: Commit
git add cfc-langgraph/app/memory/
git commit -m "feat(langgraph): 3-layer memory manager"
Files:
cfc-langgraph/app/agents/intent_classifier.pyInterfaces:
Produces: IntentClassifier.classify(query, context) → Intent 枚举
[x] Step 1: 创建 app/agents/intent_classifier.py
from enum import Enum
from langchain_openai import ChatOpenAI
from app.config import settings
import logging
logger = logging.getLogger(__name__)
class Intent(str, Enum):
CHAT = "chat" # 日常聊天 / 育儿交流
ANALYSIS = "analysis" # 报告解读 / 数据分析
RECOMMEND = "recommend" # 商品 / 活动推荐
TASK = "task" # 任务创建 / 进度查询
MIND = "mind" # 情绪疏导 / 心理支持
HEALTH = "health" # 健康咨询 / 舌诊
CLASSIFY_PROMPT = """从用户消息中识别意图, 只返回意图代码, 不要解释:
- chat: 日常聊天、育儿交流、询问建议(非具体商品/报告)
- analysis: 报告解读\数据分析\趋势查看(提到"报告"/"分析"/"评分")
- recommend: 商品\活动\文章推荐(提到"推荐"/"买"/"吃什么"/"适合")
- task: 任务相关(提到"任务"/"打卡"/"完成"/"奖励")
- mind: 情绪问题\心理支持(提到"心情"/"难过"/"焦虑"/"不开心")
- health: 健康咨询\舌诊(提到"舌"/"健康"/"体质")
用户消息: {query}
历史上下文: {context}
意图代码:
"""
class IntentClassifier:
def __init__(self):
self.llm = ChatOpenAI(
model=settings.llm_model,
api_key=settings.llm_api_key,
base_url=settings.llm_base_url,
temperature=0.1,
max_tokens=20,
)
async def classify(self, query: str, context: str = "") -> Intent:
prompt = CLASSIFY_PROMPT.format(query=query[:200], context=context[:300])
try:
resp = await self.llm.ainvoke(prompt)
intent_str = resp.content.strip().lower()
for intent in Intent:
if intent.value in intent_str:
return intent
logger.warning("无法识别的意图: %s, 默认 chat", intent_str)
return Intent.CHAT
except Exception as e:
logger.warning("意图分类失败: %s, 默认 chat", e)
return Intent.CHAT
[x] Step 2: Commit
git add cfc-langgraph/app/agents/intent_classifier.py
git commit -m "feat(langgraph): intent classifier for chat routing"
Files:
cfc-langgraph/app/agents/chat_agent.pycfc-langgraph/app/graphs/chat_graph.pyInterfaces:
MemoryManager, IntentClassifier, JavaClientProduces: ChatAgent.run() → {"answer", "conversation_id", "tasks", "sources"}
[x] Step 1: 创建 app/agents/chat_agent.py
from typing import Optional
from app.tools.java_client import JavaClient
import logging
logger = logging.getLogger(__name__)
class ChatAgent:
"""家庭聊天 Agent: 包装业务逻辑, 被 StateGraph 节点调用"""
def __init__(self):
self.java = JavaClient()
async def load_context(self, user_id: int, child_id: Optional[int] = None) -> dict:
"""从 Java 侧加载用户上下文"""
params = {}
if child_id:
params["childId"] = child_id
return await self.java.get_user_context(user_id, params)
async def extract_tasks(self, answer: str, user_id: int, conv_id: str) -> list[dict]:
"""从 AI 回答中解析 [TASK] 标记"""
import re
tasks = []
pattern = r'\[TASK:\s*\{([^}]+)\}\]'
matches = re.findall(pattern, answer)
for match in matches:
task_info = {}
for kv in match.split(","):
if ":" in kv:
k, v = kv.split(":", 1)
task_info[k.strip()] = v.strip().strip('"').strip("'")
if task_info.get("title"):
tasks.append(task_info)
return tasks
[x] Step 2: 创建 app/graphs/chat_graph.py
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint import MemorySaver
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from app.agents.intent_classifier import IntentClassifier, Intent
from app.agents.chat_agent import ChatAgent
from app.tools.product_tools import (
search_product_by_keyword,
search_article_by_keyword,
search_activity_by_keyword,
)
from app.memory.store import MemoryManager
from app.config import settings
import logging
logger = logging.getLogger(__name__)
class ChatState(TypedDict):
query: str
user_id: int
conversation_id: str
child_id: int | None
intent: Intent | None
context: dict | None
messages: list | None
answer: str | None
tasks: list[dict]
sources: list[dict]
CHAT_SYSTEM_PROMPT = """你是一个儿童成长家庭助手, 回答关于孩子成长、健康、教育的各种问题。
你可以使用搜索工具查找商品、活动和文章来辅助回答。
回答原则:
1. 用中文, 语气温暖亲切
2. 如果用户提到具体孩子, 参考提供的家庭上下文
3. 需要推荐时使用搜索工具
4. 可以生成 [TASK: {"title": "任务名", "dimension": "身/心/智/行/富", "points": 10}] 标记来创建行动任务
5. 不要编造医疗建议, 严重问题建议咨询医生
"""
def create_chat_graph():
"""创建聊天 StateGraph"""
agent = ChatAgent()
classifier = IntentClassifier()
memory_mgr = MemoryManager()
llm = ChatOpenAI(
model=settings.llm_model,
api_key=settings.llm_api_key,
base_url=settings.llm_base_url,
temperature=0.7,
)
llm_with_tools = llm.bind_tools([
search_product_by_keyword,
search_article_by_keyword,
search_activity_by_keyword,
])
builder = StateGraph(ChatState)
# ── 节点 ──
async def classify_intent(state: ChatState) -> dict:
intent = await classifier.classify(
state["query"],
context=str(state.get("context", {})),
)
return {"intent": intent}
async def load_context(state: ChatState) -> dict:
ctx = await agent.load_context(state["user_id"], state.get("child_id"))
return {"context": ctx}
async def llm_call(state: ChatState) -> dict:
"""核心 LLM 调用 + Tool"""
messages = [SystemMessage(content=CHAT_SYSTEM_PROMPT)]
# 注入家庭上下文
ctx = state.get("context", {})
if ctx:
ctx_text = f"\n家庭上下文:\n{ctx}"
messages.append(SystemMessage(content=ctx_text))
# 注入长期记忆
try:
memories = await memory_mgr.recall(state["user_id"], state["query"])
if memories:
mem_text = "\n".join([f"- {m}" for m in memories])
messages.append(SystemMessage(
content=f"相关历史对话:\n{mem_text}"
))
except Exception as e:
logger.warning("召回记忆失败: %s", e)
# 用户消息
messages.append(HumanMessage(content=state["query"]))
response = await llm_with_tools.ainvoke(messages)
answer = response.content
# 提取任务
tasks = await agent.extract_tasks(answer, state["user_id"], state["conversation_id"])
# 提取来源
sources = []
if response.response_metadata.get("tool_calls"):
for tc in response.response_metadata["tool_calls"]:
sources.append({
"type": "tool",
"name": tc.get("name", ""),
"input": tc.get("args", {}),
})
return {
"answer": answer,
"tasks": tasks,
"sources": sources,
"messages": [{"role": "user", "content": state["query"]},
{"role": "assistant", "content": answer}],
}
async def save_memory(state: ChatState) -> dict:
"""对话后保存到长期记忆"""
try:
if state.get("messages"):
await memory_mgr.save_conversation(
state["user_id"],
state["conversation_id"],
state["messages"],
)
except Exception as e:
logger.warning("保存记忆失败: %s", e)
return {}
# ── 路由 ──
def route_by_intent(state: ChatState) -> Literal["llm_call", END]:
if state["intent"] in (
Intent.RECOMMEND,
Intent.ANALYSIS,
Intent.HEALTH,
):
# 这些意图需要更专业的 Agent (Phase 3 实现)
# 当前先走通用 LLM
pass
return "llm_call"
# ── 构建图 ──
builder.add_node("classify_intent", classify_intent)
builder.add_node("load_context", load_context)
builder.add_node("llm_call", llm_call)
builder.add_node("save_memory", save_memory)
builder.add_edge(START, "classify_intent")
builder.add_edge("classify_intent", "load_context")
builder.add_conditional_edges("load_context", route_by_intent)
builder.add_edge("llm_call", "save_memory")
builder.add_edge("save_memory", END)
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
return graph
[x] Step 3: Commit
git add cfc-langgraph/app/agents/chat_agent.py cfc-langgraph/app/graphs/chat_graph.py
git commit -m "feat(langgraph): ChatAgent StateGraph with intent routing and memory"
Files:
cfc-langgraph/app/api/chat.pyModify: cfc-langgraph/app/main.py (注册路由)
[x] Step 1: 创建 app/api/chat.py
from fastapi import APIRouter
from app.models.chat import ChatRequest, ChatResponse, SourceInfo
from app.graphs.chat_graph import create_chat_graph
router = APIRouter(prefix="/api/v1", tags=["chat"])
# 编译图 (单例)
_graph = None
def get_graph():
global _graph
if _graph is None:
_graph = create_chat_graph()
return _graph
@router.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
"""家庭聊天: 意图分类→上下文→LLM→记忆"""
graph = get_graph()
initial_state = {
"query": req.query,
"user_id": req.user_id,
"conversation_id": req.conversation_id,
"child_id": req.context.child_id if req.context else None,
"intent": None,
"context": None,
"messages": None,
"answer": None,
"tasks": [],
"sources": [],
}
# 配置: 使用 conversation_id 作为线程 ID, 支持历史续接
config = {
"configurable": {"thread_id": req.conversation_id or str(req.user_id)},
}
result = await graph.ainvoke(initial_state, config)
sources = []
for s in result.get("sources", []):
sources.append(SourceInfo(
type=s.get("type", "tool"),
title=s.get("name", ""),
))
conv_id = req.conversation_id or f"conv_{req.user_id}_{__import__('time').time()}"
return ChatResponse(
answer=result.get("answer", ""),
conversation_id=conv_id,
sources=sources,
tasks=result.get("tasks", []),
)
[x] Step 2: 修改 app/main.py 注册路由
from app.api import health, recommend, chat # 新增 chat
app.include_router(chat.router) # 新增
[x] Step 3: Commit
git add cfc-langgraph/app/api/chat.py cfc-langgraph/app/main.py
git commit -m "feat(langgraph): chat API endpoint with LangGraph graph"
Files:
Modify: cfc-langgraph/app/rag/retriever.py (升级为 ensemble + compression)
[x] Step 1: 重写 app/rag/retriever.py
from langchain_chroma import Chroma
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain.retrievers import ContextualCompressionRetriever
from langchain_openai import ChatOpenAI
from .embeddings import get_embeddings
from app.config import settings
from app.tools.java_client import JavaClient
from typing import Optional
import logging
logger = logging.getLogger(__name__)
class RagRetriever:
"""升级版混合检索器: 向量 + BM25 + LLM 压缩重排序"""
def __init__(self, collection_name: str = "cfc_knowledge"):
embeddings = get_embeddings()
self.vectorstore = Chroma(
collection_name=collection_name,
embedding_function=embeddings,
persist_directory=settings.chroma_db_path,
)
self.java_client = JavaClient()
self._bm25_retriever: Optional[BM25Retriever] = None
self._bm25_texts: list[str] = []
async def initialize(self):
"""从 Java 侧拉取知识库, 构建 BM25 索引"""
try:
articles = await self.java_client.get_published_articles()
self._bm25_texts = [
f"{a['title']} {a['summary']} {a.get('tags', '')}"
for a in articles
]
if self._bm25_texts:
self._bm25_retriever = BM25Retriever.from_texts(
self._bm25_texts,
metadatas=articles,
)
logger.info("BM25 索引就绪: %d 条", len(self._bm25_texts))
except Exception as e:
logger.warning("BM25 初始化失败: %s", e)
async def retrieve(
self,
query: str,
filters: Optional[dict] = None,
k: int = 5,
use_compression: bool = True,
) -> list[dict]:
"""混合检索 + 可选 LLM 压缩重排序"""
retrievers = []
# 1. 向量检索
vector_retriever = self.vectorstore.as_retriever(
search_kwargs={"k": k * 2, "filter": filters},
)
retrievers.append(vector_retriever)
# 2. BM25 关键词检索
if self._bm25_retriever:
bm25_k = self._bm25_retriever.k
self._bm25_retriever.k = k * 2
retrievers.append(self._bm25_retriever)
self._bm25_retriever.k = bm25_k
if len(retrievers) == 1:
docs = await retrievers[0].ainvoke(query)
else:
ensemble = EnsembleRetriever(
retrievers=retrievers,
weights=[0.6, 0.4],
)
docs = await ensemble.ainvoke(query)
# 3. LLM 压缩 (剔除不相关内容)
if use_compression and docs:
llm = ChatOpenAI(
model=settings.llm_model,
api_key=settings.llm_api_key,
base_url=settings.llm_base_url,
temperature=0,
)
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=ensemble if len(retrievers) > 1 else retrievers[0],
)
docs = await compression_retriever.ainvoke(query)
# 4. 格式化为统一输出
results = []
seen = set()
for doc in docs:
content_hash = hash(doc.page_content[:100])
if content_hash in seen:
continue
seen.add(content_hash)
results.append({
"content": doc.page_content,
"metadata": doc.metadata,
"score": doc.metadata.get("score", 0) if hasattr(doc, "metadata") else 0,
})
return results[:k]
[x] Step 2: Commit
git add cfc-langgraph/app/rag/retriever.py
git commit -m "feat(langgraph): RAG pipeline upgraded to ensemble + compression"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/AiContextService.javacfc-backend/src/main/java/com/etotem/cfc/controller/ai/ContextApiController.javaInterfaces:
Produces: POST /api/ai/context (Python 可调用的 HTTP 数据接口, 接收 user_id + intent_type + params)
[x] Step 1: 创建 ContextApiController.java
package com.etotem.cfc.controller.ai;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.service.AiContextService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Map;
/**
* 上下文数据 API (供 LangGraph Python 服务调用)
* 替代 Dify Workflow HTTP 回调的 /api/ai/context
*/
@Tag(name = "AI上下文", description = "供 LangGraph 服务调用的上下文数据接口")
@RestController("aiContextApiController")
@RequestMapping("/api/ai/context")
public class ContextApiController {
@Resource
private AiContextService aiContextService;
@Operation(summary = "获取 AI 上下文数据", hidden = true)
@PostMapping("")
public Result<Map<String, Object>> getContext(@RequestBody Map<String, Object> params) {
String userIdStr = (String) params.get("user_id");
String intentType = (String) params.get("intent_type");
Map<String, Object> contextParams = (Map<String, Object>) params.get("params");
if (userIdStr == null) {
return Result.error("缺少参数: user_id");
}
Long userId = Long.valueOf(userIdStr);
Map<String, Object> ctx = aiContextService.getContext(intentType, userId, contextParams);
return Result.success(ctx);
}
}
[x] Step 2: 编译验证
cd cfc-backend
mvn clean compile -q
[x] Step 3: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/controller/ai/ContextApiController.java
git commit -m "feat(backend): context API for LangGraph Python service"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/AIService.javaModify: cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java (新增 chat 方法)
[x] Step 1: AiGateway 新增 chat() 方法
// 在 AiGateway.java 中追加
/**
* 调用 Python ChatAgent, 失败时返回 null
*/
public Map<String, Object> chat(String query, Long userId, String conversationId,
Map<String, Object> inputs) {
if (!enabled || isCircuitOpen()) return null;
try {
ObjectNode body = objectMapper.createObjectNode();
body.put("query", query);
body.put("user_id", userId);
body.put("conversation_id", conversationId != null ? conversationId : "");
if (inputs != null && !inputs.isEmpty()) {
ObjectNode ctx = body.putObject("context");
inputs.forEach((key, value) -> {
if (value instanceof String) ctx.put(key, (String) value);
else if (value instanceof Number) ctx.put(key, ((Number) value).doubleValue());
else if (value instanceof Boolean) ctx.put(key, (Boolean) value);
});
}
HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
String url = baseUrl + "/api/v1/chat";
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
JsonNode root = objectMapper.readTree(response.getBody());
Map<String, Object> result = new LinkedHashMap<>();
result.put("answer", root.get("answer").asText());
result.put("conversationId", root.get("conversation_id").asText());
result.put("tasks", parseTasks(root.get("tasks")));
consecutiveFailures.set(0);
return result;
}
return null;
} catch (Exception e) {
log.warn("AiGateway chat 调用失败: {}", e.getMessage());
recordFailure();
return null;
}
}
private List<Map<String, Object>> parseTasks(JsonNode tasksNode) {
List<Map<String, Object>> tasks = new ArrayList<>();
if (tasksNode != null && tasksNode.isArray()) {
for (JsonNode task : tasksNode) {
Map<String, Object> t = new LinkedHashMap<>();
t.put("title", task.get("title").asText());
t.put("dimension", task.has("dimension") ? task.get("dimension").asText() : "");
t.put("rewardPoints", task.has("points") ? task.get("points").asInt() : 0);
tasks.add(t);
}
}
return tasks;
}
[x] Step 2: 修改 AIService.sendMessage(), 增加 Python 优先路由
// 在 AIService 中注入 AiGateway
@Resource
private AiGateway aiGateway;
/**
* 发送聊天消息: 优先 Python LangGraph, 失败回退 Dify
*/
public Map<String, Object> sendMessage(String query, String userId,
String conversationId,
Map<String, Object> inputs) {
Long uid = Long.valueOf(userId);
// 1. 尝试 Python LangGraph
Map<String, Object> pythonResult = aiGateway.chat(query, uid, conversationId, inputs);
if (pythonResult != null) {
log.debug("LangGraph chat 成功: userId={}", userId);
// 镜像到本地 (兼容现有 ChatMirrorService)
mirrorConversation(uid,
(String) pythonResult.getOrDefault("conversationId", conversationId),
"family", query, (String) pythonResult.get("answer"), inputs);
return pythonResult;
}
// 2. Fallback: Dify
log.info("LangGraph 不可用, fallback to Dify: userId={}", userId);
return sendMessageToDify(query, userId, conversationId, inputs);
}
/**
* 原有 Dify 发送逻辑 (提取为独立方法)
*/
private Map<String, Object> sendMessageToDify(String query, String userId,
String conversationId,
Map<String, Object> inputs) {
String url = difyBaseUrl + "/chat-messages";
Map<String, Object> body = new LinkedHashMap<>();
body.put("query", query);
body.put("user", userId);
body.put("response_mode", "blocking");
body.put("conversation_id", conversationId != null ? conversationId : "");
body.put("inputs", inputs != null ? inputs : Collections.emptyMap());
body.put("auto_generate_name", true);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, authHeaders());
ResponseEntity<Map> resp = restTemplate.postForEntity(url, entity, Map.class);
Map<String, Object> result = new LinkedHashMap<>();
if (resp.getBody() != null) {
result.put("answer", resp.getBody().get("answer"));
result.put("conversationId", resp.getBody().get("conversation_id"));
mirrorConversation(Long.valueOf(userId),
(String) resp.getBody().get("conversation_id"),
"family", query, (String) resp.getBody().get("answer"), inputs);
}
return result;
}
[x] Step 3: 编译验证
cd cfc-backend
mvn clean compile -q
[x] Step 4: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
git add cfc-backend/src/main/java/com/etotem/cfc/service/AIService.java
git commit -m "feat(backend): AIService routes to LangGraph with Dify fallback"
Files:
cfc-langgraph/tests/test_chat.pycfc-langgraph/tests/test_memory.pyCreate: cfc-langgraph/tests/test_intent.py
[x] Step 1: 创建 tests/test_intent.py
import pytest
from app.agents.intent_classifier import IntentClassifier, Intent
@pytest.mark.asyncio
async def test_classify_chat():
classifier = IntentClassifier()
intent = await classifier.classify("今天天气真好")
assert intent == Intent.CHAT
@pytest.mark.asyncio
async def test_classify_analysis():
classifier = IntentClassifier()
intent = await classifier.classify("帮我看看小明的健康报告")
assert intent == Intent.ANALYSIS
@pytest.mark.asyncio
async def test_classify_recommend():
classifier = IntentClassifier()
intent = await classifier.classify("推荐一些补钙的食物")
assert intent == Intent.RECOMMEND
@pytest.mark.asyncio
async def test_classify_mind():
classifier = IntentClassifier()
intent = await classifier.classify("孩子最近心情不好")
assert intent == Intent.MIND
[x] Step 2: 创建 tests/test_memory.py
import pytest
from app.memory.store import MemoryManager
@pytest.mark.asyncio
async def test_memory_manager_init():
mgr = MemoryManager()
assert mgr is not None
assert mgr.memory_vectorstore is not None
@pytest.mark.asyncio
async def test_working_memory():
mgr = MemoryManager()
mem = mgr.get_working_memory()
assert mem is not None
assert mem.memory_key == "history"
[x] Step 3: 创建 tests/test_chat.py
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
@pytest.mark.asyncio
async def test_chat_endpoint():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.post("/api/v1/chat", json={
"query": "你好",
"user_id": 1,
"conversation_id": "",
})
assert resp.status_code == 200
data = resp.json()
assert "answer" in data
assert isinstance(data.get("tasks"), list)
[x] Step 4: 运行测试
cd cfc-langgraph
pytest tests/ -v
# 预期: test_intent 通过 (需要 LLM API Key)
# test_memory 通过
# test_chat 通过 (需要 Java 后端 + LLM API Key)
[x] Step 5: Commit
git add cfc-langgraph/tests/
git commit -m "test(langgraph): Phase 2 tests for chat, memory, intent"
POST /api/v1/chat 完整链路