187 lines
7.3 KiB
Python
187 lines
7.3 KiB
Python
"""
|
||
混合模式客服系统 - 支持业务知识+通用知识+实时工具
|
||
"""
|
||
from typing import TypedDict, Literal
|
||
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
|
||
from langchain_openai import ChatOpenAI
|
||
from langgraph.graph import StateGraph, END
|
||
from langgraph.checkpoint.memory import MemorySaver
|
||
|
||
from config import LLM_API_KEY, LLM_BASE_URL, LLM_MODEL, SYSTEM_PROMPT
|
||
from knowledge import get_knowledge_base
|
||
|
||
|
||
class HybridState(TypedDict):
|
||
"""混合模式状态"""
|
||
messages: list
|
||
intent: Literal["medical", "general", "realtime", "greeting"]
|
||
context: str
|
||
tool_result: str
|
||
session_id: str
|
||
|
||
|
||
class HybridChatbot:
|
||
"""混合客服:垂直领域 + 通用知识 + 实时工具"""
|
||
|
||
def __init__(self):
|
||
self.llm = ChatOpenAI(
|
||
model=LLM_MODEL,
|
||
api_key=LLM_API_KEY,
|
||
base_url=LLM_BASE_URL,
|
||
temperature=0.7
|
||
)
|
||
self.knowledge_base = get_knowledge_base()
|
||
self.workflow = self._build_workflow()
|
||
|
||
def _classify_intent(self, state: HybridState) -> HybridState:
|
||
"""意图识别 - 判断问题类型"""
|
||
query = state["messages"][-1].content
|
||
|
||
# 用LLM快速判断意图(也可以用关键词规则)
|
||
prompt = f"""判断用户意图,只能返回以下之一:
|
||
- medical: 医疗/健康相关问题
|
||
- general: 通用知识(历史、科学、生活常识等)
|
||
- realtime: 需要实时数据(天气、股价、新闻等)
|
||
- greeting: 问候/闲聊
|
||
|
||
用户问题:{query}
|
||
意图:"""
|
||
|
||
response = self.llm.invoke([HumanMessage(content=prompt)])
|
||
intent = response.content.strip().lower()
|
||
|
||
# 归一化
|
||
valid_intents = ["medical", "general", "realtime", "greeting"]
|
||
intent = intent if intent in valid_intents else "general"
|
||
|
||
print(f"🎯 意图识别: {query} -> {intent}")
|
||
return {"intent": intent, "messages": state["messages"], "session_id": state["session_id"]}
|
||
|
||
def _route_by_intent(self, state: HybridState) -> str:
|
||
"""根据意图路由到不同节点"""
|
||
intent = state.get("intent", "general")
|
||
|
||
routing_map = {
|
||
"medical": "retrieve_knowledge", # 查医疗知识库
|
||
"general": "general_answer", # 直接问大模型
|
||
"realtime": "call_tool", # 调用实时工具
|
||
"greeting": "general_answer" # 闲聊也走通用
|
||
}
|
||
|
||
return routing_map.get(intent, "general_answer")
|
||
|
||
def _retrieve_medical(self, state: HybridState) -> HybridState:
|
||
"""检索医疗知识库"""
|
||
query = state["messages"][-1].content
|
||
docs = self.knowledge_base.search(query)
|
||
|
||
if docs:
|
||
context = "\n".join([doc.page_content for doc in docs])
|
||
return {"context": context, "messages": state["messages"],
|
||
"intent": state["intent"], "session_id": state["session_id"]}
|
||
else:
|
||
# 医疗问题但知识库没有 → 转人工
|
||
return {"context": "【无相关医疗知识,需转人工】",
|
||
"messages": state["messages"],
|
||
"intent": state["intent"], "session_id": state["session_id"]}
|
||
|
||
def _call_realtime_tool(self, state: HybridState) -> HybridState:
|
||
"""调用实时工具(MCP/Function Call)"""
|
||
query = state["messages"][-1].content
|
||
|
||
# 示例:天气查询(实际可用 MCP/天气API)
|
||
if "天气" in query:
|
||
# 这里可以调用 MCP 或天气API
|
||
# result = call_mcp("weather", location="北京")
|
||
tool_result = "【实时天气工具】:北京今天晴,25°C,适合出行"
|
||
elif "股价" in query or "股票" in query:
|
||
tool_result = "【实时股价工具】:需要调用股票API获取实时数据"
|
||
else:
|
||
tool_result = "【无可用工具】"
|
||
|
||
return {"tool_result": tool_result, "messages": state["messages"],
|
||
"intent": state["intent"], "session_id": state["session_id"]}
|
||
|
||
def _generate_answer(self, state: HybridState) -> HybridState:
|
||
"""生成最终回答"""
|
||
intent = state["intent"]
|
||
query = state["messages"][-1].content
|
||
|
||
if intent == "medical":
|
||
# 医疗问题:基于知识库回答
|
||
if "无相关医疗知识" in state.get("context", ""):
|
||
response = "抱歉,这个问题我暂时无法回答,为您转接专业医生..."
|
||
else:
|
||
system_msg = f"你是医疗助手,基于以下知识回答:\n{state['context']}"
|
||
messages = [SystemMessage(content=system_msg)] + state["messages"]
|
||
response = self.llm.invoke(messages).content
|
||
|
||
elif intent == "realtime":
|
||
# 实时信息:基于工具结果回答
|
||
system_msg = f"基于以下实时信息回答用户:\n{state.get('tool_result', '')}"
|
||
messages = [SystemMessage(content=system_msg)] + state["messages"]
|
||
response = self.llm.invoke(messages).content
|
||
|
||
else: # general / greeting
|
||
# 通用问题:直接回答
|
||
system_msg = "你是友好的助手,回答用户的通用问题。"
|
||
messages = [SystemMessage(content=system_msg)] + state["messages"]
|
||
response = self.llm.invoke(messages).content
|
||
|
||
return {"messages": state["messages"] + [AIMessage(content=response)],
|
||
"intent": intent, "session_id": state["session_id"]}
|
||
|
||
def _build_workflow(self):
|
||
"""构建工作流"""
|
||
workflow = StateGraph(HybridState)
|
||
|
||
# 添加节点
|
||
workflow.add_node("classify", self._classify_intent)
|
||
workflow.add_node("retrieve_knowledge", self._retrieve_medical)
|
||
workflow.add_node("call_tool", self._call_realtime_tool)
|
||
workflow.add_node("general_answer", self._generate_answer)
|
||
workflow.add_node("generate", self._generate_answer)
|
||
|
||
# 条件路由
|
||
workflow.set_entry_point("classify")
|
||
workflow.add_conditional_edges(
|
||
"classify",
|
||
self._route_by_intent,
|
||
{
|
||
"retrieve_knowledge": "retrieve_knowledge",
|
||
"call_tool": "call_tool",
|
||
"general_answer": "general_answer"
|
||
}
|
||
)
|
||
|
||
# 汇聚到生成节点
|
||
workflow.add_edge("retrieve_knowledge", "generate")
|
||
workflow.add_edge("call_tool", "generate")
|
||
workflow.add_edge("generate", END)
|
||
workflow.add_edge("general_answer", END)
|
||
|
||
return workflow.compile(checkpointer=MemorySaver())
|
||
|
||
async def chat(self, message: str, session_id: str) -> str:
|
||
"""处理消息"""
|
||
config = {"configurable": {"thread_id": session_id}}
|
||
result = await self.workflow.ainvoke(
|
||
{"messages": [HumanMessage(content=message)],
|
||
"intent": "general",
|
||
"context": "",
|
||
"tool_result": "",
|
||
"session_id": session_id},
|
||
config
|
||
)
|
||
return result["messages"][-1].content
|
||
|
||
|
||
# 单例
|
||
_hybrid_chatbot = None
|
||
|
||
def get_hybrid_chatbot():
|
||
global _hybrid_chatbot
|
||
if _hybrid_chatbot is None:
|
||
_hybrid_chatbot = HybridChatbot()
|
||
return _hybrid_chatbot
|