Files
Learn-LLM-RAG/app/generator.py
T
2026-10-10 23:11:55 +08:00

86 lines
3.5 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.
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