Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
"""
|
||||
Q&A 自动拆分分块策略
|
||||
|
||||
使用 LLM 将文档内容自动拆分为问答对。
|
||||
每个分段的 content 存储 question,metadata 中存储 answer。
|
||||
检索时用 question 做向量匹配,返回 answer 作为上下文。
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from .base import BaseChunker, ChunkResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Q&A 拆分的系统提示词
|
||||
QA_SYSTEM_PROMPT = """你是一个专业的知识库问答对生成助手。请根据给定的文本内容,生成高质量的问答对(Q&A pairs)。
|
||||
|
||||
要求:
|
||||
1. 问题应该是用户可能会问的自然语言问题
|
||||
2. 答案应该准确、完整,直接来源于原文
|
||||
3. 每个问答对应该覆盖文本中的一个独立知识点
|
||||
4. 问题要具体明确,避免过于宽泛
|
||||
5. 答案要简洁但完整,包含必要的上下文
|
||||
|
||||
请以 JSON 数组格式输出,每个元素包含 question 和 answer 字段:
|
||||
```json
|
||||
[
|
||||
{"question": "问题1", "answer": "答案1"},
|
||||
{"question": "问题2", "answer": "答案2"}
|
||||
]
|
||||
```
|
||||
|
||||
只输出 JSON 数组,不要输出其他内容。"""
|
||||
|
||||
|
||||
class QAChunker(BaseChunker):
|
||||
"""
|
||||
Q&A 自动拆分分块器
|
||||
|
||||
使用 LLM 将文本拆分为问答对。
|
||||
需要在初始化时传入 LLM 调用函数。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
chunk_size: int = 500,
|
||||
chunk_overlap: int = 50,
|
||||
separator: Optional[str] = None,
|
||||
llm_caller: Optional[Any] = None,
|
||||
):
|
||||
super().__init__(chunk_size, chunk_overlap, separator)
|
||||
self._llm_caller = llm_caller
|
||||
|
||||
def chunk(self, text: str, metadata: Dict[str, Any] = None) -> List[ChunkResult]:
|
||||
"""
|
||||
同步分块(Q&A 模式不支持同步调用,返回空列表)
|
||||
请使用 chunk_async 方法。
|
||||
"""
|
||||
logger.warning("QAChunker.chunk() 不支持同步调用,请使用 chunk_async()")
|
||||
return []
|
||||
|
||||
async def chunk_async(self, text: str, metadata: Dict[str, Any] = None) -> List[ChunkResult]:
|
||||
"""
|
||||
异步分块:使用 LLM 将文本拆分为 Q&A 对
|
||||
|
||||
Args:
|
||||
text: 原始文本
|
||||
metadata: 文档元数据
|
||||
|
||||
Returns:
|
||||
分块结果列表,每个 ChunkResult 的 content 为 question,
|
||||
metadata 中包含 answer 和 chunk_mode='qa'
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
return []
|
||||
|
||||
metadata = metadata or {}
|
||||
text = self._clean_text(text)
|
||||
|
||||
# 如果文本太长,先按段落粗分再逐段生成 Q&A
|
||||
max_input_size = self.chunk_size * 8 # LLM 输入上限
|
||||
if len(text) > max_input_size:
|
||||
segments = self._split_for_qa(text, max_input_size)
|
||||
else:
|
||||
segments = [text]
|
||||
|
||||
all_results = []
|
||||
position = 0
|
||||
|
||||
for segment in segments:
|
||||
qa_pairs = await self._generate_qa_pairs(segment)
|
||||
for qa in qa_pairs:
|
||||
question = qa.get('question', '').strip()
|
||||
answer = qa.get('answer', '').strip()
|
||||
if not question or not answer:
|
||||
continue
|
||||
|
||||
all_results.append(ChunkResult(
|
||||
content=question,
|
||||
position=position,
|
||||
metadata={
|
||||
**metadata,
|
||||
'answer': answer,
|
||||
'chunk_mode': 'qa',
|
||||
},
|
||||
))
|
||||
position += 1
|
||||
|
||||
logger.info(f"Q&A 拆分完成: {len(all_results)} 个问答对")
|
||||
return all_results
|
||||
|
||||
async def _generate_qa_pairs(self, text: str) -> List[Dict[str, str]]:
|
||||
"""调用 LLM 生成 Q&A 对"""
|
||||
if not self._llm_caller:
|
||||
logger.error("QAChunker: 未配置 LLM 调用函数")
|
||||
return []
|
||||
|
||||
try:
|
||||
user_prompt = f"请根据以下文本生成问答对:\n\n{text}"
|
||||
|
||||
response_text = await self._llm_caller(
|
||||
system_prompt=QA_SYSTEM_PROMPT,
|
||||
user_prompt=user_prompt,
|
||||
)
|
||||
|
||||
if not response_text:
|
||||
return []
|
||||
|
||||
# 解析 JSON 响应
|
||||
return self._parse_qa_response(response_text)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Q&A 生成失败: {e}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _parse_qa_response(response_text: str) -> List[Dict[str, str]]:
|
||||
"""解析 LLM 返回的 Q&A JSON"""
|
||||
try:
|
||||
# 尝试直接解析
|
||||
result = json.loads(response_text)
|
||||
if isinstance(result, list):
|
||||
return [
|
||||
item for item in result
|
||||
if isinstance(item, dict) and 'question' in item and 'answer' in item
|
||||
]
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# 尝试提取 JSON 代码块
|
||||
import re
|
||||
json_match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', response_text, re.DOTALL)
|
||||
if json_match:
|
||||
try:
|
||||
result = json.loads(json_match.group(1))
|
||||
if isinstance(result, list):
|
||||
return [
|
||||
item for item in result
|
||||
if isinstance(item, dict) and 'question' in item and 'answer' in item
|
||||
]
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# 尝试找到 [ ... ] 部分
|
||||
bracket_match = re.search(r'\[.*\]', response_text, re.DOTALL)
|
||||
if bracket_match:
|
||||
try:
|
||||
result = json.loads(bracket_match.group(0))
|
||||
if isinstance(result, list):
|
||||
return [
|
||||
item for item in result
|
||||
if isinstance(item, dict) and 'question' in item and 'answer' in item
|
||||
]
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
logger.warning(f"无法解析 Q&A 响应: {response_text[:200]}")
|
||||
return []
|
||||
|
||||
def _split_for_qa(self, text: str, max_size: int) -> List[str]:
|
||||
"""将长文本按段落分割为适合 LLM 处理的片段"""
|
||||
paragraphs = text.split('\n\n')
|
||||
segments = []
|
||||
current = ""
|
||||
|
||||
for para in paragraphs:
|
||||
if current and len(current) + len(para) + 2 > max_size:
|
||||
segments.append(current.strip())
|
||||
current = para
|
||||
else:
|
||||
current = current + "\n\n" + para if current else para
|
||||
|
||||
if current.strip():
|
||||
segments.append(current.strip())
|
||||
|
||||
return segments
|
||||
Reference in New Issue
Block a user