refactor(kb): 移除QA分割功能并改进分隔符处理

移除不再使用的QA分割功能及相关代码,简化文本分块逻辑
添加分隔符转义处理以支持前端传入的转义字符
改进分块参数配置界面的描述和默认值
This commit is contained in:
Wenjie Zhang 2026-01-02 14:20:18 +08:00
parent c5fcbc6374
commit 450911eaad
8 changed files with 56 additions and 81 deletions

View File

@ -14,7 +14,6 @@ from src.knowledge.utils.kb_utils import (
get_embedding_config,
prepare_item_metadata,
split_text_into_chunks,
split_text_into_qa_chunks,
)
from src.utils import logger
from src.utils.datetime_utils import utc_isoformat
@ -151,16 +150,7 @@ class ChromaKB(KnowledgeBase):
def _split_text_into_chunks(self, text: str, file_id: str, filename: str, params: dict) -> list[dict]:
"""将文本分割成块"""
# 检查是否使用QA分割模式
use_qa_split = params.get("use_qa_split", False)
if use_qa_split:
# 使用QA分割模式
qa_separator = params.get("qa_separator", "\n\n\n")
chunks = split_text_into_qa_chunks(text, file_id, filename, qa_separator, params)
else:
# 使用传统分割模式
chunks = split_text_into_chunks(text, file_id, filename, params)
chunks = split_text_into_chunks(text, file_id, filename, params)
# 为 ChromaDB 添加特定的 metadata 格式
for chunk in chunks:

View File

@ -14,7 +14,6 @@ from src.knowledge.utils.kb_utils import (
get_embedding_config,
prepare_item_metadata,
split_text_into_chunks,
split_text_into_qa_chunks,
)
from src.models.embed import OtherEmbedding
from src.utils import hashstr, logger
@ -222,16 +221,7 @@ class MilvusKB(KnowledgeBase):
def _split_text_into_chunks(self, text: str, file_id: str, filename: str, params: dict) -> list[dict]:
"""将文本分割成块"""
# 检查是否使用QA分割模式
use_qa_split = params.get("use_qa_split", False)
if use_qa_split:
# 使用QA分割模式
qa_separator = params.get("qa_separator", "\n\n\n")
return split_text_into_qa_chunks(text, file_id, filename, qa_separator, params)
else:
# 使用传统分割模式
return split_text_into_chunks(text, file_id, filename, params)
return split_text_into_chunks(text, file_id, filename, params)
async def add_content(self, db_id: str, items: list[str], params: dict | None = {}) -> list[dict]:
"""添加内容(文件/URL"""

View File

@ -11,7 +11,6 @@ from .kb_utils import (
merge_processing_params,
prepare_item_metadata,
split_text_into_chunks,
split_text_into_qa_chunks,
validate_file_path,
)
@ -20,7 +19,6 @@ __all__ = [
"get_embedding_config",
"prepare_item_metadata",
"split_text_into_chunks",
"split_text_into_qa_chunks",
"merge_processing_params",
"validate_file_path",
]

View File

@ -74,6 +74,23 @@ def validate_file_path(file_path: str, db_id: str = None) -> str:
raise ValueError(f"Invalid file path: {file_path}")
def _unescape_separator(separator: str | None) -> str | None:
"""将前端传入的字面量转义字符转换为实际字符
例如: "\\n\\n\\n" -> "\n\n\n"
"""
if not separator:
return None
# 处理常见的转义序列
separator = separator.replace("\\n", "\n")
separator = separator.replace("\\r", "\r")
separator = separator.replace("\\t", "\t")
separator = separator.replace("\\\\", "\\")
return separator
def split_text_into_chunks(text: str, file_id: str, filename: str, params: dict = {}) -> list[dict]:
"""
将文本分割成块使用 LangChain MarkdownTextSplitter 进行智能分割
@ -82,6 +99,16 @@ def split_text_into_chunks(text: str, file_id: str, filename: str, params: dict
chunk_size = params.get("chunk_size", 1000)
chunk_overlap = params.get("chunk_overlap", 200)
# 获取分隔符并转换为实际字符
separator = params.get("qa_separator")
separator = _unescape_separator(separator)
# 向后兼容:如果旧配置设置了 use_qa_split=True 但未指定 separator使用默认分隔符
use_qa_split = params.get("use_qa_split", False)
if use_qa_split and not separator:
separator = "\n\n\n"
logger.debug("启用了向后兼容模式use_qa_split=True使用默认分隔符 \\n\\n\\n")
# 使用 MarkdownTextSplitter 进行智能分割
# MarkdownTextSplitter 会尝试沿着 Markdown 格式的标题进行分割
text_splitter = MarkdownTextSplitter(
@ -89,7 +116,18 @@ def split_text_into_chunks(text: str, file_id: str, filename: str, params: dict
chunk_overlap=chunk_overlap,
)
text_chunks = text_splitter.split_text(text)
# 如果设置了分隔符,先分割后以当前的分割逻辑处理
if separator:
# 转换分隔符为可视格式(换行符显示为 \n
separator_display = separator.replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t')
logger.debug(f"启用预分割模式,使用分隔符: '{separator_display}'")
pre_chunks = text.split(separator)
text_chunks = []
for pre_chunk in pre_chunks:
if pre_chunk.strip():
text_chunks.extend(text_splitter.split_text(pre_chunk))
else:
text_chunks = text_splitter.split_text(text)
# 转换为标准格式
for chunk_index, chunk_content in enumerate(text_chunks):
@ -136,7 +174,8 @@ async def calculate_content_hash(data: bytes | bytearray | str | os.PathLike[str
return sha256.hexdigest()
raise TypeError(f"Unsupported data type for hashing: {type(data)!r}")
# 理论上不会执行到这里,但保留作为防御性编程
raise TypeError(f"Unsupported data type for hashing: {type(data)!r}") # type: ignore[unreachable]
async def prepare_item_metadata(item: str, content_type: str, db_id: str, params: dict | None = None) -> dict:
@ -223,39 +262,6 @@ async def prepare_item_metadata(item: str, content_type: str, db_id: str, params
return metadata
def split_text_into_qa_chunks(
text: str, file_id: str, filename: str, qa_separator: None | str = None, params: dict = {}
) -> list[dict]:
"""
将文本按QA对分割成块使用 LangChain CharacterTextSplitter 进行分割"""
qa_separator = qa_separator or "\n\n"
text_chunks = text.split(qa_separator)
# 转换为标准格式
chunks = []
for chunk_index, chunk_content in enumerate(text_chunks):
if chunk_content.strip(): # 跳过空块
chunk_content = chunk_content.strip()[:4096]
chunks.append(
{
"id": f"{file_id}_qa_chunk_{chunk_index}",
"content": chunk_content.strip(),
"file_id": file_id,
"filename": filename,
"chunk_index": chunk_index,
"source": filename,
"chunk_id": f"{file_id}_qa_chunk_{chunk_index}",
"chunk_type": "qa", # 标识为QA类型的chunk
}
)
logger.debug(f"QA chunks: {chunks[0]}")
logger.debug(
f"Successfully split QA text into {len(chunks)} chunks using CharacterTextSplitter with `{qa_separator=}`"
)
return chunks
def merge_processing_params(metadata_params: dict | None, request_params: dict | None) -> dict:
"""
合并处理参数优先使用请求参数缺失时使用元数据中的参数

View File

@ -17,13 +17,13 @@
<a-input-number v-model:value="tempChunkParams.chunk_overlap" :min="0" :max="1000" style="width: 100%;" />
<p class="param-description">相邻文本片段间的重叠字符数</p>
</a-form-item>
<a-form-item v-if="showQaSplit" label="QA分割模式" name="use_qa_split">
<a-switch v-model:checked="tempChunkParams.use_qa_split" />
<p class="param-description">启用后将按QA对分割忽略上述chunk大小设置</p>
</a-form-item>
<a-form-item v-if="tempChunkParams?.use_qa_split && showQaSplit" label="QA分隔符" name="qa_separator">
<a-input v-model:value="tempChunkParams.qa_separator" placeholder="输入QA分隔符" style="width: 100%;" />
<p class="param-description">用于分割不同QA对的分隔符</p>
<a-form-item v-if="showQaSplit" label="分隔符 (Separator)" name="qa_separator">
<a-input
v-model:value="tempChunkParams.qa_separator"
placeholder="输入分隔符,例如 \n\n\n 或 ---"
style="width: 100%;"
/>
<p class="param-description">支持 \n\t 等转义字符留空则不启用预分割</p>
</a-form-item>
</a-form>
</div>

View File

@ -379,8 +379,7 @@ const rechunkModalLoading = computed(() => store.state.chunkLoading);
const rechunkParams = ref({
chunk_size: 1000,
chunk_overlap: 200,
use_qa_split: false,
qa_separator: '\n\n\n'
qa_separator: '\\n\\n\\n'
});
const currentRechunkFileIds = ref([]);
const isBatchRechunk = ref(false);
@ -788,8 +787,7 @@ const handleRechunkConfirm = async () => {
rechunkParams.value = {
chunk_size: 1000,
chunk_overlap: 200,
use_qa_split: false,
qa_separator: '\n\n\n'
qa_separator: '\\n\\n\\n'
};
} else {
message.error(`重新分块失败: ${result.message}`);
@ -811,8 +809,7 @@ const handleRechunkCancel = () => {
rechunkParams.value = {
chunk_size: 1000,
chunk_overlap: 200,
use_qa_split: false,
qa_separator: '\n\n\n'
qa_separator: '\\n\\n\\n'
};
};

View File

@ -365,8 +365,7 @@ const chunkParams = ref({
chunk_size: 1000,
chunk_overlap: 200,
enable_ocr: 'disable',
use_qa_split: false,
qa_separator: '\n\n\n',
qa_separator: '',
});
//
@ -376,8 +375,7 @@ const chunkConfigModalVisible = ref(false);
const tempChunkParams = ref({
chunk_size: 1000,
chunk_overlap: 200,
use_qa_split: false,
qa_separator: '\n\n\n',
qa_separator: '',
});
// QA
@ -739,7 +737,6 @@ const showChunkConfigModal = () => {
tempChunkParams.value = {
chunk_size: chunkParams.value.chunk_size,
chunk_overlap: chunkParams.value.chunk_overlap,
use_qa_split: isQaSplitSupported.value ? chunkParams.value.use_qa_split : false,
qa_separator: chunkParams.value.qa_separator,
};
chunkConfigModalVisible.value = true;
@ -750,10 +747,7 @@ const handleChunkConfigSubmit = () => {
chunkParams.value.chunk_overlap = tempChunkParams.value.chunk_overlap;
// QAQA
if (isQaSplitSupported.value) {
chunkParams.value.use_qa_split = tempChunkParams.value.use_qa_split;
chunkParams.value.qa_separator = tempChunkParams.value.qa_separator;
} else {
chunkParams.value.use_qa_split = false;
}
chunkConfigModalVisible.value = false;
message.success('分块参数配置已更新');

View File

@ -114,7 +114,7 @@ const currentChunk = ref(null);
const tooltipStyle = ref({ top: '0px', left: '0px' });
const activeChunkIndex = ref(null);
const highlightedChunkIndex = ref(null);
const chunkPanelVisible = ref(true);
const chunkPanelVisible = ref(false);
// -
const theme = computed(() => themeStore.isDark ? 'dark' : 'light');