Files
DPM/backend/app/rag.py
T
Pine f4067c1f0e refactor(s2s): 语音助手基础提示词迁移到后端统一管理
- 后端 config.py:新增 S2S_INSTRUCTIONS(.env DPM_S2S_INSTRUCTIONS 可覆盖)
- rag.py:build_instructions() 组装 配置提示词 + 园区知识库摘要
- routers.py:GET /api/s2s/instructions 下发(前端连接时拉取)
- 前端 VoiceAssistant:删除硬编码 DEFAULT_INSTRUCTIONS,改拉取后端指令;
  仅保留后端不可用时的极简兜底

改提示词只需改后端(.env 或代码),前端零硬编码。
2026-08-18 02:28:55 +08:00

116 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""园区知识库 —— 轻量本地 RAGembedding + numpy 余弦检索)
- 知识源:backend/knowledge/park.md(按标题/段落分块)
- 向量化:dashscope text-embedding-v3(云端 API,索引构建一次后缓存)
- 检索:numpy 余弦 top-k(资料量小,暴力检索足够)
- 提供:
- retrieve(query, top_k) → 相关段落(供对话动态注入)
- brief() → 固定知识摘要(供前端拼进 instructions,静态知识一次注入)
"""
import json
import logging
import os
from pathlib import Path
import numpy as np
log = logging.getLogger("dpm.rag")
KB_FILE = Path(__file__).resolve().parent.parent / "knowledge" / "park.md"
INDEX_FILE = Path(__file__).resolve().parent.parent / "knowledge" / "kb_index.json"
EMBED_MODEL = "text-embedding-v3"
_index = None # 缓存 {chunks, embeddings}
def _chunk_md(text: str) -> list[str]:
"""按标题/段落分块"""
chunks: list[str] = []
cur: list[str] = []
for line in text.splitlines():
if line.startswith("#"):
if cur:
chunks.append("\n".join(cur).strip())
cur = [line]
else:
cur.append(line)
if cur:
chunks.append("\n".join(cur).strip())
return [c for c in chunks if len(c) > 10]
def _embed(texts: list[str]) -> list[list[float]]:
"""dashscope text-embedding-v3 批量向量化"""
import dashscope
api_key = os.environ.get("DASHSCOPE_API_KEY") or ""
resp = dashscope.TextEmbedding.call(
model=EMBED_MODEL,
input=texts,
api_key=api_key,
)
if resp.status_code != 200:
raise RuntimeError(f"embedding API {resp.status_code}: {resp.code} {resp.message}")
out = [e["embedding"] for e in resp.output["embeddings"]]
return out
def _load_index():
"""懒加载索引;缺失时构建并缓存"""
global _index
if _index is not None:
return _index
if INDEX_FILE.exists():
try:
_index = json.loads(INDEX_FILE.read_text("utf-8"))
log.info("rag: 知识索引已加载(%d 块)", len(_index["chunks"]))
return _index
except Exception: # noqa: BLE001
log.warning("rag: 索引损坏,重建")
# 构建
chunks = _chunk_md(KB_FILE.read_text("utf-8"))
log.info("rag: 构建知识索引(%d 块,embedding 中…)", len(chunks))
embs = _embed(chunks)
_index = {"chunks": chunks, "embeddings": embs}
INDEX_FILE.write_text(json.dumps(_index, ensure_ascii=False), "utf-8")
log.info("rag: 知识索引构建完成 -> %s", INDEX_FILE)
return _index
def retrieve(query: str, top_k: int = 3) -> list[str]:
"""检索与 query 最相关的知识段落"""
if not query or not query.strip():
return []
idx = _load_index()
q = _embed([query])[0]
sims = np.dot(np.array(idx["embeddings"]), np.array(q))
top = np.argsort(-sims)[:top_k]
return [idx["chunks"][int(i)] for i in top if float(sims[int(i)]) > 0.3]
def brief(max_chars: int = 1200) -> str:
"""固定知识摘要(前端拼进 instructions,静态知识一次注入)"""
idx = _load_index()
out = []
total = 0
for c in idx["chunks"]:
if total + len(c) > max_chars:
break
out.append(c)
total += len(c)
return "\n\n".join(out)
def build_instructions() -> str:
"""语音助手的完整基础提示词 = 配置提示词 + 园区知识库摘要(服务端统一组装)"""
from .config import settings
base = settings.S2S_INSTRUCTIONS.strip()
try:
kb = brief()
if kb:
base += f"\n\n【园区知识库】\n{kb}"
except Exception as e: # noqa: BLE001
log.warning("rag: 知识库拼接失败(仅用基础提示词): %s", e)
return base