Update project and configurations
This commit is contained in:
238
intelligent_cabin/app/services/knowledge_llm.py
Normal file
238
intelligent_cabin/app/services/knowledge_llm.py
Normal file
@@ -0,0 +1,238 @@
|
||||
"""
|
||||
app/services/knowledge_llm.py
|
||||
|
||||
当 BERT NLU 未命中时,使用 LLM + knowledge_search function call 查询本地知识库。
|
||||
|
||||
流程:
|
||||
1. 构建 tools=[knowledge_search] 发给 LLM
|
||||
2. 若 LLM 返回 tool_calls → 执行 KnowledgeStore.search() → 拼结果再发一次 LLM
|
||||
3. LLM 生成最终回复 reply_text + knowledge_doc_id(可选)
|
||||
|
||||
返回 KnowledgeReplyResult,包含:
|
||||
- reply_text: 简短自然语言摘要
|
||||
- doc_id / doc_content: 命中的知识文档(供前端渲染 KnowledgeArtifact)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from urllib import error, request
|
||||
|
||||
from app.services.knowledge_store import KnowledgeDoc, KnowledgeStore
|
||||
|
||||
|
||||
@dataclass
|
||||
class KnowledgeReplyResult:
|
||||
reply_text: str
|
||||
backend: str
|
||||
model_name: str
|
||||
doc_id: str | None = None
|
||||
doc_content: str | None = None # 原始 MD 内容,前端渲染用
|
||||
doc_title: str | None = None
|
||||
error_message: str | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
# ── LLM 工具定义(OpenAI function calling 格式,DashScope 兼容)────────────────
|
||||
_KNOWLEDGE_SEARCH_TOOL: dict[str, Any] = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "knowledge_search",
|
||||
"description": (
|
||||
"搜索本地设备知识库,获取焊管机/弯管机产线相关的故障排查、操作规程等知识。"
|
||||
"当用户问到设备故障、报警处理、操作方法、工艺参数时请调用此工具。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "搜索关键词,如'虚焊报警'、'激光扫描仪操作'、'弯管模具调节'等",
|
||||
}
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_SYSTEM_PROMPT = """\
|
||||
你是焊管机产线智能助手,负责回答操作工人关于设备故障、工艺调节、仪器使用的问题。
|
||||
你有一个工具 knowledge_search 可以查询本地设备知识库,遇到设备类问题时请先调用它。
|
||||
回答时语言简洁、口语化,先给出结论,再说步骤,总长度不超过 100 字。
|
||||
如果工具返回了相关知识,请基于知识内容回答,不要编造。
|
||||
如果没有找到相关知识,诚实告知"暂未找到相关资料,建议联系技术支持"。
|
||||
"""
|
||||
|
||||
|
||||
class DashScopeKnowledgeLLM:
|
||||
"""使用 DashScope(OpenAI 兼容 API)+ function calling 的知识库问答器。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
model_name: str,
|
||||
knowledge_store: KnowledgeStore,
|
||||
timeout_seconds: float = 12.0,
|
||||
max_tool_rounds: int = 2,
|
||||
) -> None:
|
||||
self._base_url = base_url.rstrip("/")
|
||||
self._api_key = api_key
|
||||
self._model_name = model_name
|
||||
self._store = knowledge_store
|
||||
self._timeout = timeout_seconds
|
||||
self._max_tool_rounds = max_tool_rounds
|
||||
|
||||
# ── 主入口 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def reply(self, user_text: str) -> KnowledgeReplyResult:
|
||||
"""完整 function-call 对话流(最多 max_tool_rounds 轮工具调用)。"""
|
||||
if not self._base_url or not self._api_key or not self._model_name:
|
||||
return self._local_fallback(user_text, "LLM not configured")
|
||||
|
||||
messages: list[dict[str, Any]] = [
|
||||
{"role": "system", "content": _SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_text},
|
||||
]
|
||||
|
||||
found_doc: KnowledgeDoc | None = None
|
||||
|
||||
for _round in range(self._max_tool_rounds):
|
||||
raw = self._chat(messages, tools=[_KNOWLEDGE_SEARCH_TOOL])
|
||||
if raw is None:
|
||||
return self._local_fallback(user_text, "LLM request failed")
|
||||
|
||||
choice = self._first_choice(raw)
|
||||
if choice is None:
|
||||
return self._local_fallback(user_text, "empty choices")
|
||||
|
||||
finish_reason = choice.get("finish_reason", "")
|
||||
message = choice.get("message", {})
|
||||
|
||||
# ── 工具调用分支 ─────────────────────────────────────────────────
|
||||
if finish_reason == "tool_calls" or message.get("tool_calls"):
|
||||
tool_calls = message.get("tool_calls", [])
|
||||
messages.append({"role": "assistant", **message})
|
||||
|
||||
for tc in tool_calls:
|
||||
fn_name = tc.get("function", {}).get("name", "")
|
||||
fn_args_raw = tc.get("function", {}).get("arguments", "{}")
|
||||
tc_id = tc.get("id", "call_0")
|
||||
|
||||
if fn_name == "knowledge_search":
|
||||
try:
|
||||
fn_args = json.loads(fn_args_raw)
|
||||
except json.JSONDecodeError:
|
||||
fn_args = {"query": user_text}
|
||||
|
||||
query = fn_args.get("query", user_text)
|
||||
tool_result, found_doc = self._run_knowledge_search(query)
|
||||
else:
|
||||
tool_result = f"Unknown tool: {fn_name}"
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tc_id,
|
||||
"content": tool_result,
|
||||
})
|
||||
# 继续下一轮 LLM 调用
|
||||
continue
|
||||
|
||||
# ── 正常文本回复 ─────────────────────────────────────────────────
|
||||
content = self._extract_content(message)
|
||||
if not content:
|
||||
return self._local_fallback(user_text, "empty content")
|
||||
|
||||
return KnowledgeReplyResult(
|
||||
reply_text=content,
|
||||
backend="dashscope",
|
||||
model_name=self._model_name,
|
||||
doc_id=found_doc.doc_id if found_doc else None,
|
||||
doc_content=found_doc.content if found_doc else None,
|
||||
doc_title=found_doc.title if found_doc else None,
|
||||
)
|
||||
|
||||
# 超出工具调用轮数,直接本地兜底
|
||||
return self._local_fallback(user_text, "max tool rounds exceeded")
|
||||
|
||||
# ── 内部工具执行 ───────────────────────────────────────────────────────────
|
||||
|
||||
def _run_knowledge_search(self, query: str) -> tuple[str, KnowledgeDoc | None]:
|
||||
"""执行本地知识库搜索,返回 (tool_result_str, best_doc)。"""
|
||||
results = self._store.search(query, top_k=2)
|
||||
if not results:
|
||||
return "未找到相关知识文档。", None
|
||||
|
||||
best = results[0]
|
||||
# 给 LLM 的 tool result:文档标题 + 正文(截断到 800 字节)
|
||||
excerpt = best.doc.content[:800]
|
||||
tool_text = (
|
||||
f"[知识库检索结果]\n"
|
||||
f"文档:{best.doc.title}\n"
|
||||
f"命中关键词:{', '.join(best.matched_keywords)}\n\n"
|
||||
f"{excerpt}"
|
||||
)
|
||||
return tool_text, best.doc
|
||||
|
||||
# ── HTTP 调用 ──────────────────────────────────────────────────────────────
|
||||
|
||||
def _chat(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: list[dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
payload: dict[str, Any] = {
|
||||
"model": self._model_name,
|
||||
"temperature": 0.3,
|
||||
"enable_thinking": False,
|
||||
"max_tokens": 300,
|
||||
"messages": messages,
|
||||
}
|
||||
if tools:
|
||||
payload["tools"] = tools
|
||||
payload["tool_choice"] = "auto"
|
||||
|
||||
req = request.Request(
|
||||
self._endpoint(),
|
||||
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self._api_key}",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with request.urlopen(req, timeout=self._timeout) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except (error.URLError, TimeoutError, ValueError, OSError):
|
||||
return None
|
||||
|
||||
def _endpoint(self) -> str:
|
||||
if self._base_url.endswith("/chat/completions"):
|
||||
return self._base_url
|
||||
return f"{self._base_url}/chat/completions"
|
||||
|
||||
def _first_choice(self, payload: dict[str, Any]) -> dict[str, Any] | None:
|
||||
choices = payload.get("choices")
|
||||
if not isinstance(choices, list) or not choices:
|
||||
return None
|
||||
return choices[0]
|
||||
|
||||
def _extract_content(self, message: dict[str, Any]) -> str:
|
||||
content = message.get("content", "")
|
||||
if isinstance(content, list):
|
||||
return "".join(
|
||||
str(item.get("text", "")).strip()
|
||||
for item in content
|
||||
if isinstance(item, dict) and item.get("type") == "text"
|
||||
).strip()
|
||||
return str(content).strip()
|
||||
|
||||
def _local_fallback(self, _user_text: str, reason: str) -> KnowledgeReplyResult:
|
||||
return KnowledgeReplyResult(
|
||||
reply_text="暂未找到相关资料,建议联系技术支持或查阅设备手册。",
|
||||
backend="local-fallback",
|
||||
model_name="knowledge-fallback",
|
||||
error_message=reason,
|
||||
)
|
||||
Reference in New Issue
Block a user