Init: 第一个版本
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
|
||||
from app.config import LOW_SCORE, llm_config
|
||||
|
||||
logger = logging.getLogger("learn_rag")
|
||||
|
||||
SYSTEM_PROMPT = """你是「知迹」的知识库问答助手。只能根据给出的知识切片回答,不要补充切片之外的信息,也不要编造。
|
||||
如果切片不足以回答,就说明知识库里没有这项信息。
|
||||
用自然的中文直接回答问题,不要使用 Markdown,不要写标题。
|
||||
引用事实时在相关句末标注编号,格式必须是〔来源1〕这种全角括号。编号只能使用给定切片的编号。"""
|
||||
|
||||
|
||||
def build_context(chunks: list[dict]) -> str:
|
||||
blocks = []
|
||||
for chunk in chunks:
|
||||
blocks.append(
|
||||
f"〔来源{chunk['n']}〕《{chunk['title']}》相关度 {chunk['score']:.0%}\n{chunk['text']}"
|
||||
)
|
||||
return "\n\n".join(blocks)
|
||||
|
||||
|
||||
def extractive_answer(chunks: list[dict], low_confidence: bool) -> str:
|
||||
if not chunks:
|
||||
return "知识库里还没有可以检索的内容。"
|
||||
lead = ""
|
||||
if low_confidence:
|
||||
lead = "最接近的切片和问题只是部分相关,下面按原文摘出,请以右侧切片为准。\n\n"
|
||||
lines = [lead + chunks[0]["text"].strip() + f"〔来源{chunks[0]['n']}〕"]
|
||||
for chunk in chunks[1:2]:
|
||||
if chunks[0]["score"] - chunk["score"] <= 0.08:
|
||||
lines.append("\n" + chunk["text"].strip() + f"〔来源{chunk['n']}〕")
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
|
||||
def generate(question: str, chunks: list[dict]) -> tuple[str, str, str | None, bool]:
|
||||
low_confidence = not chunks or chunks[0]["score"] < LOW_SCORE
|
||||
if not chunks:
|
||||
return "知识库里还没有可以检索的内容。", "extractive", None, True
|
||||
|
||||
settings = llm_config()
|
||||
if settings is None:
|
||||
notice = "还没有配置大模型,回答直接摘自相关度最高的切片。在项目目录的 .env 中填入 OPENAI_API_KEY 后重启,即可生成连贯回答。"
|
||||
return extractive_answer(chunks, low_confidence), "extractive", notice, low_confidence
|
||||
|
||||
caution = ""
|
||||
if low_confidence:
|
||||
caution = "注意:最高相关度偏低。如果切片不能直接支持答案,请明确说知识库里没有这项信息。\n\n"
|
||||
user_prompt = f"{caution}知识切片如下:\n\n{build_context(chunks)}\n\n问题:{question}"
|
||||
try:
|
||||
answer = call_llm(settings, user_prompt)
|
||||
except Exception:
|
||||
logger.exception("大模型调用失败")
|
||||
notice = "大模型暂时不可用,已改为直接摘录检索到的切片。"
|
||||
return extractive_answer(chunks, low_confidence), "extractive", notice, low_confidence
|
||||
return answer, "llm", None, low_confidence
|
||||
|
||||
|
||||
def call_llm(settings: dict, user_prompt: str) -> str:
|
||||
response = httpx.post(
|
||||
f"{settings['base']}/chat/completions",
|
||||
headers={"Authorization": f"Bearer {settings['key']}"},
|
||||
json={
|
||||
"model": settings["model"],
|
||||
"temperature": 0.2,
|
||||
"max_tokens": 800,
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_prompt},
|
||||
],
|
||||
},
|
||||
timeout=httpx.Timeout(60.0, connect=10.0),
|
||||
)
|
||||
response.raise_for_status()
|
||||
message = response.json()["choices"][0]["message"]
|
||||
content = message.get("content")
|
||||
if isinstance(content, list):
|
||||
content = "".join(
|
||||
part.get("text", "") if isinstance(part, dict) else str(part) for part in content
|
||||
)
|
||||
text = (content or "").strip()
|
||||
if not text:
|
||||
raise ValueError("模型返回了空回答")
|
||||
return text
|
||||
Reference in New Issue
Block a user