Files
MemoryBear/docs/rag/pipeline/01-loader-parser-chunking.md
Multica PM Agent 343a5eebe3
Some checks failed
Sync to Gitee / sync (push) Has been cancelled
docs(rag): add MemoryBear RAG implementation docs v1.0
Submit the formed RAG documentation set produced across Sprint-1/2/3
(WS-12 through WS-26) under docs/rag/. Includes:

- README.md / INDEX.md: landing + total index (responsibility matrix,
  review verdicts, dual-link to source issues)
- overview/: full-pipeline architecture (4 .mmd diagrams),
  11-stage boundary contracts, doc map, source-code inventory
- pipeline/: 5 deep-dives (Loader/Parser/Chunking, Embedding,
  VDB & retrieval, GraphRAG, Rerank/Prompt/LLM)
- graphrag/, end-to-end/: v1.0 formal versions with full source
  retained as reference
- evolution/: 11 architecture-refactor proposals,
  6-direction roadmap, capability map
- review/: S3-T1 / S3-T2 final reviews, S2-T7 final summary
- _indexes/: glossary (81 terms), source->doc reverse index, chart index
- _release/: v1.0-RC1 release manifest, versioning convention,
  ops & freshness plan
- _meta/README.md: placeholder noting WS-12 governance assets gap

Aggregate review score 92.6/100 (8/8 PASS, 31/31 source-code spot
checks hit). The legacy docs/ ignore in .gitignore is narrowed to
docs/* with an explicit allowlist for docs/rag/.

Refs: WS-26
Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 10:51:48 +08:00

47 KiB
Raw Blame History

title, author, last-reviewed-at, source-commit, scope
title author last-reviewed-at source-commit scope
[S2-T1] 文档加载与预处理Loader / Parser / Chunking实现详解 Python 开发工程师 2026-05-08 HEAD (origin/main, MemoryBear) api/app/core/rag/{crawler, integrations, deepdoc, nlp, models, utils, app/naive.py, common/token_utils.py}

0. 一句话定位

把"任意来源、任意格式"的原始资料,沉淀为带元数据、可被 Embedding/索引消费的标准化 Chunk 序列;这一段是 RAG 召回质量的"硬天花板"——它做不好,下游再多优化都救不回来。

1. 设计目标与适用场景

目标 落地策略
多源接入(爬虫 / 飞书 / 语雀 / 本地文件) crawler/integrations/feishuintegrations/yuque 三套 SDK均落到本地文件后再走统一 chunk() 入口
多格式解析PDF/Word/Excel/PPT/HTML/MD/JSON/TXT/图片/音视频) app/naive.py:chunk() 单一编排入口,按扩展名分派到 deepdoc/parser/*app/{audio,picture}.py
复杂 PDF 还原(表格、图、版面) RAGPdfParser + OCR + 版面识别 + TSR + XGBoost 段落连接模型
长文 Chunking 既保语义又控 token naive_merge / naive_merge_docx / hierarchical_merge / tree_merge 多种策略,统一以 cl100k_base 计算 token
同一篇资料的多模态(图 + 文 + 表) tokenize_chunks_with_imagestokenize_table 把图片/表格作为附属信息挂在 chunk 上
健壮性 鉴权 token 缓存、退避重试、robots.txt 合规、编码自动嗅探、嵌入文件递归解构

适用于:私有知识库、企业文档库、技术资料归档;不适用于:实时流式数据、对端到端延迟<200ms 的场景OCR 与版面识别是 CPU/GPU 重负载)。

2. 术语表

  • Section:解析器吐出的中间结构 (text, position_or_layout) 元组列表,是 Chunking 之前的"原料"。
  • Chunk:最终交给 Embedding 的文本片段,一般 ≤ chunk_token_num 个 token默认 128512
  • Token:用 tiktoken.cl100k_base 编码后得到的 BPE token 数(与 OpenAI gpt-4 同口径)。
  • Layout页面区块类别title / text / figure / table / equation 等),由 YOLOv10 检测。
  • TSRTable Structure Recognition复杂表格行/列/合并单元格的结构还原。
  • OCR:文字检测 + 文字识别两阶段的图像字符抽取。
  • Embed file:内嵌在 docx/xlsx/pptx 内部的子文件(如 docx 里嵌的 Excel需递归解析。

3. 实现概览(数据流图)

flowchart LR
    subgraph Loader["Loader / 多源接入"]
        A1[本地文件] --> CHUNK
        A2[Web 站点] --> WC[WebCrawler<br/>BFS 同域]
        A3[飞书云文档] --> FS[FeishuAPIClient<br/>导出/下载]
        A4[语雀知识库] --> YQ[YuqueAPIClient<br/>raw markdown]
        WC --> CD[CrawledDocument<br/>title+content]
        FS --> LF[本地文件]
        YQ --> LF
        CD --> CHUNK
        LF --> CHUNK
    end

    subgraph Parser["Parser / 格式分派"]
        CHUNK[app/naive.py: chunk] --> EX[extract_embed_file<br/>嵌入文件递归]
        CHUNK -->|.pdf| PARSERS[PARSERS dict<br/>deepdoc/mineru/textln/plaintext]
        CHUNK -->|.docx| DOCX[Docx/RAGDocxParser]
        CHUNK -->|.xlsx/.csv| XLS[RAGExcelParser]
        CHUNK -->|.md| MD[Markdown/RAGMarkdownParser]
        CHUNK -->|.html| HTML[RAGHtmlParser]
        CHUNK -->|.json/.jsonl| JSON[RAGJsonParser]
        CHUNK -->|.txt/code| TXT[RAGTxtParser]
        CHUNK -->|.ppt/.pptx| LO[LibreOffice<br/>convert_to_pdf]
        CHUNK -->|.doc| TIKA[Apache Tika]
        CHUNK -->|图片/音视频| MM[picture/audio<br/>vision_llm_chunk]
        LO --> PARSERS
        PARSERS --> OCR[OCR + LayoutRecognizer + TSR]
        DOCX --> SEC[(sections)]
        XLS --> SEC
        MD --> SEC
        HTML --> SEC
        JSON --> SEC
        TXT --> SEC
        OCR --> SEC
        TIKA --> SEC
    end

    subgraph Chunking["Chunking / 切分 + 索引化"]
        SEC --> NM{有图片?}
        NM -->|否| NM1[naive_merge]
        NM -->|是·docx| NM2[naive_merge_docx]
        NM -->|是·md| NM3[naive_merge_with_images]
        NM1 --> TC[tokenize_chunks]
        NM2 --> TCI[tokenize_chunks_with_images]
        NM3 --> TCI
        TT[tokenize_table] --> ESDOC
        TC --> ESDOC[(ES Doc<br/>content_with_weight<br/>content_ltks<br/>page_num_int<br/>position_int<br/>image)]
        TCI --> ESDOC
    end

4. Loader 章节

4.1 Web Crawlercrawler/

  • 入口WebCrawler(entry_url, max_pages, delay_seconds, timeout_seconds, user_agent, include_patterns, exclude_patterns),源码 api/app/core/rag/crawler/web_crawler.py:19
  • 架构BFSdeque + visited_urls+ 五个独立组件:URLNormalizer / RobotsParser / RateLimiter / HTTPFetcher / ContentExtractor,全部通过组合而非继承装配,便于替换。
  • 同域限制URLNormalizer.is_same_domain() 强制只爬入口域名,避免无界扩散(url_normalizer.py:102-124)。
  • 去重URLNormalizer.normalize() 做:小写 host、去 fragment、去默认端口、剥离 utm_*/fbclid/gclid 等追踪参数、按字母序排 query。url_normalizer.py:28-100
  • robots.txt 合规RobotsParser.can_fetch()get_crawl_delay(),使用 stdlib 的 urllib.robotparser每域名缓存。robots.txt 拉取失败时默认允许permissive fallbackrobots_parser.py:60-69
  • 限速RateLimiter 默认 1s/请求,set_delay() 可被 Crawl-delay 动态覆盖(上限 60s 防呆);backoff(2.0) 用于 429/503 指数退避,rate_limiter.py:38-58
  • HTTP 重试HTTPFetcher 内置 max_retries=3,退避 1s → 2s → 4s429 与 503 显式触发重试404/4xx 立即返回不重试5xx 重试到耗尽。http_fetcher.py:54-180
  • 编码处理HTTPFetcher._get_decoded_content 五级回退HTML meta charset → response.encoding跳过 ISO-8859-1 → UTF-8 → GBK/Big5/Shift-JIS/EUC-KR 等 → latin-1 with errors='replace'。http_fetcher.py:182-248
  • 正文抽取ContentExtractor.extract 基于 lxml:移除 script/style/nav/header/footer/aside,按 <article>/<main>[role=main]class/id =~ content|main|article|post<body> 顺序找主体;用 is_static_content 检测"脚本多文本少"的 SPA 页面并直接跳过。content_extractor.py:24-72
  • 错误统计stats.error_breakdown 记录每种错误类型的计数,便于事后分析。web_crawler.py:210-215
# api/app/core/rag/crawler/web_crawler.py:103-145节选
while self.url_queue and self.pages_processed < self.max_pages:
    url = self.url_queue.popleft()
    if url in self.visited_urls: continue
    self.visited_urls.add(url)
    if not self.robots_parser.can_fetch(url):       # robots.txt
        self.stats['skipped'] += 1; continue
    self.rate_limiter.wait()                        # 限速
    fetch_result = self.http_fetcher.fetch(url)     # 重试 + 退避
    if not fetch_result.success:
        self._record_error(fetch_result.error or "Unknown error"); continue
    content_type = fetch_result.headers.get('Content-Type', '').lower()
    if not any(s in content_type for s in ['text/html', 'application/xhtml+xml']):
        self.stats['skipped'] += 1; continue        # 非 HTML 跳过
    extracted = self.content_extractor.extract(fetch_result.content, url)
    if not extracted.is_static:
        self.stats['skipped'] += 1; continue        # JS-only 站点跳过

4.2 飞书集成(integrations/feishu/

  • 入口FeishuAPIClient(app_id, app_secret, api_base_url, timeout, max_retries)integrations/feishu/client.py:24,是异步客户端(httpx.AsyncClient),用 async with 管理生命周期。
  • 鉴权tenant_access_token 模式,get_tenant_access_token()cachetools.TTLCache(maxsize=1, ttl=7200-300) 缓存(飞书原生 2 小时有效,提前 5 分钟失效)+ asyncio.Lock 双检锁防并发请求 token。client.py:51-127
  • 文件类型分派download_documentdocument.type 分两条路径:
    • 在线文档doc/docx/sheet/bitable_export_file 走"创建导出任务 → 轮询 ticket → 下载 file_token"三步,最多轮询 10 次、间隔 2s超时抛 FeishuAPIErrorclient.py:311-406
    • 附件文件file/slides_download_file 直接 GET /drive/v1/files/{token}/download,从 Content-Disposition 解析 filename*=UTF-8''xxx 编码文件名。client.py:408-452
  • 限流与重试:装饰器 @with_retryfeishu/retry.py:124-137)。RetryStrategy.RETRYABLE_ERRORS = (FeishuNetworkError, FeishuRateLimitError, httpx.TimeoutException/ConnectError/ReadError)MAX_RETRIES=3,退避 [1, 2, 4]sHTTP 429/502/503/5xx 重试4xx除 429不重试飞书业务码 99991400/99991401(限流码)也强制重试。feishu/retry.py:24-76
  • 错误模型:精细化异常树 FeishuAuthError / FeishuAPIError / FeishuNotFoundError / FeishuPermissionError / FeishuRateLimitError / FeishuNetworkError / FeishuDataError,调用方据此决定告警级别。feishu/exceptions.py:1-46
  • 分页与递归list_folder_files 单页page_size=200list_all_folder_files(recursive=True) 自动展开子文件夹。client.py:226-269
# api/app/core/rag/integrations/feishu/client.py:78-120鉴权 + 双检锁缓存)
cached_token = self._token_cache.get("access_token")
if cached_token: return cached_token
async with self._token_lock:
    cached_token = self._token_cache.get("access_token")
    if cached_token: return cached_token
    response = await self._http_client.post(
        "/auth/v3/tenant_access_token/internal",
        json={"app_id": self.app_id, "app_secret": self.app_secret})
    data = response.json()
    if data.get("code") != 0:
        raise FeishuAuthError(f"Authentication failed: {data.get('msg')}",
                              error_code=str(data.get("code")), details=data)
    token = data.get("tenant_access_token")
    self._token_cache["access_token"] = token
    return token

4.3 语雀集成(integrations/yuque/

  • 入口YuqueAPIClient(user_id, token, api_base_url, timeout, max_retries)integrations/yuque/client.py:27
  • 鉴权:个人 PATHTTP header X-Auth-Token,无需 OAuth/token 刷新(语雀的 token 是长期 token故没有 token 缓存层。client.py:55-66
  • API 三段式get_user_repos()get_repo_docs(book_id)get_doc_detail(id, raw=1)get_doc_detailparams={"raw": 1} 拉原始 markdown。client.py:119-291
  • 格式分派download_document:根据 doc.format 决定本地文件后缀:
    • markdown / lake.mdlake 也按 markdown 保存,因为 lake 在 raw 模式下输出兼容 md
    • html.html
    • lakesheet.xlsx,需 zlib.decompress(bytes(sheet_data, 'latin-1')) 解压后由 generate_excel_from_sheet 用 openpyxl 重建工作簿(含字体、对齐、颜色、合并单元格)。client.py:293-545
  • 限流与重试:与飞书同构,yuque/retry.py:21-118RetryStrategy 配置一致HTTP 状态码 401→YuqueAuthError、403→YuquePermissionError、404→YuqueNotFoundError、429→YuqueRateLimitError,由 _handle_api_error 统一翻译。client.py:73-117
  • 健壮性get_user_repos/get_repo_docs 对单条数据 try/except 跳过坏记录而不整体失败(容忍语雀 schema 漂移)。client.py:158-160, 221-223

4.4 本地文件(app/naive.py:chunk

  • 是所有 Loader 的最终汇入口;接收 filenamebinary 两种入参,二者互斥(推荐 binary,源码内 extract_embed_file 显式不支持 path 模式,详见 app/naive.py:541)。
  • 嵌入文件递归:根调用(is_root=True)会先用 extract_embed_file() 抽出 docx/xlsx/pptx 内部嵌入的子文件(通过 zip 名单 word/embeddings/xl/embeddings/ppt/embeddings/ 或 OLE 容器的 Ole10Native),逐个递归 chunk(),结果合入 embed_resutils/file_utils.py:69-130 + app/naive.py:533-552
  • 超链接深挖parser_config.analyze_hyperlink=Truedocx/pdf 内部超链接经 extract_links_from_docx / extract_links_from_pdf 抽出后,每条 URL 调用 extract_html 拉回 HTML 二进制并递归 chunk(url, html_bytes, is_root=False)app/naive.py:556-566, 793-803
  • callback 进度上报chunk(..., callback=progress_callback),约定 callback(prog: float, msg: str)关键节点0.05(嵌入抽取)/ 0.1(开始解析)/ 0.6OCR 完)/ 0.63(版面)/ 0.65(表格)/ 0.67(合并)/ 0.8(解析完)。

5. Parser 章节

5.1 总分派器(app/naive.py

chunk() 是入口,按文件扩展名走分支:

# api/app/core/rag/app/naive.py:97-102
PARSERS = {
    "deepdoc":  by_deepdoc,
    "mineru":   by_mineru,
    "textln":   by_textln,
    "plaintext": by_plaintext,  # default
}
# api/app/core/rag/app/naive.py:553-764
if re.search(r"\.docx$", filename, re.IGNORECASE): ...
elif re.search(r"\.pdf$", filename, re.IGNORECASE): ...      # 走 PARSERS dict
elif re.search(r"\.(pptx|ppt?)$", ...): ...                  # LibreOffice → pdf
elif re.search(r"\.(da|wav|mp3|...)$", ...): ...             # app/audio.py
elif re.search(r"\.(png|jpeg|...)$", ...): ...               # app/picture.py
elif re.search(r"\.(csv|xlsx?)$", ...): ExcelParser
elif re.search(r"\.(txt|py|js|java|...)$", ...): TxtParser
elif re.search(r"\.(md|markdown)$", ...): Markdown(MarkdownParser 子类)
elif re.search(r"\.(htm|html)$", ...): HtmlParser
elif re.search(r"\.(json|jsonl|ldjson)$", ...): JsonParser
elif re.search(r"\.doc$", ...): tika  # Apache Tika via JVM

PDF 的 parser_config.layout_recognize 决定底层走哪条 PDF 引擎,默认 DeepDOC

layout_recognize 引擎 调用 适用
DeepDOC Pdf(RAGPdfParser) by_deepdoc 复杂版面、扫描件
Plain Text PlainParser by_plaintext 纯文本 PDF速度快
MinerU MinerUParser by_mineru 高质量结构化(外部进程或 HTTP
TextLn TextLnParser by_textln TextIn API云端付费
任意(含 vision_model VisionParser by_plaintext 分支 多模态 LLM 直读

5.2 PDF 解析(deepdoc/parser/pdf_parser.py1387 行)

RAGPdfParser 是大头,调用栈:

# api/app/core/rag/app/naive.py:373-412 Pdf.__call__ 节选)
self.__images__(filename if not binary else binary, zoomin, from_page, to_page, callback)
callback(0.6, f"OCR finished")
self._layouts_rec(zoomin)              # 版面识别
callback(0.63, "Layout analysis")
self._table_transformer_job(zoomin)    # TSR
callback(0.65, "Table analysis")
self._text_merge(zoomin=zoomin)        # 文本合并
self._extract_table_figure(...)        # 提取表与图
self._naive_vertical_merge()
self._concat_downward()                # XGBoost 段落连接updown_concat_xgb
self._final_reading_order_merge()
return [(b["text"], self._line_tag(b, zoomin)) for b in self.boxes], tbls

要点:

  • OCROCR()deepdoc/vision/ocr.py:522= TextDetector + TextRecognizer 组合;pdfplumber 把每一页 to_image(resolution=72*zoomin=216).annotated,再过 OCR。pdf_parser.py:1006-1122
  • 版面识别LayoutRecognizer4YOLOv10默认10 个 labeltitle / Text / Reference / Figure / Figure caption / Table / Table caption / Equation 等),或 AscendLayoutRecognizerAscend NPULAYOUT_RECOGNIZER_TYPE 环境变量切换。pdf_parser.py:53-67 + vision/layout_recognizer.py:147-160
  • 表格结构识别TableStructureRecognizervision/table_structure_recognizer.py),裁出 table 区域后把行/列重组成 HTML与 docx 的"按上下文找最近标题"风格一致。pdf_parser.py:178-220
  • 段落连接模型updown_cnt_mdlXGBoost输入是上下相邻两块的 31 维特征句末是否标点、x0 距离、行内 token 数、字号差、layout_type 等),决定要不要把下一块续到上一块。pdf_parser.py:113-156 + pdf_parser.py:70-83(模型从 HuggingFace InfiniFlow/text_concat_xgb_v1.0 拉)。
  • 位置标签:每个文本块带 @@<page>\t<x0>\t<x1>\t<top>\t<bottom>## 的位置 tagremove_tag()re.sub(r"@@[\t0-9.-]+?##", "", txt) 去掉,extract_positions() 反解。pdf_parser.py:1219-1229
  • GPU 加速:通过 pip_install_torch() + torch.cuda.is_available() 把 XGBoost 推到 CUDAPARALLEL_DEVICES > 1 时用 trio.CapacityLimiter 做多卡并行。pdf_parser.py:50-77, 1095-1106
  • HuggingFace 模型分发InfiniFlow/text_concat_xgb_v1.0 通过 snapshot_download 拉到 res/ 目录;推荐 export HF_ENDPOINT=https://hf-mirror.com 解决国内拉取慢,deepdoc/README.md:42

5.2.1 备选 PDF 引擎

  • PlainParserpdf_parser.py:1300pypdf.PdfReader 直接 extract_text(),每行一段 + 解析 outline 目录;无 OCR、无版面、无图纯文本极快。
  • VisionParserpdf_parser.py:1334):把每一页转成 PIL.Image整页直接喂给 vision_modelQWenCV / AzureGptV4 等),让多模态 LLM "看图说话"产出 markdown。@@page\tx0\tx1\ty0\ty1## 位置 tag 由 (0, 0, width/zoomin, 0, height/zoomin) 占位生成(即整页矩形),方便下游对齐 chunk 与原图。
  • MinerUParsermineru_parser.py:41):调用外部 mineru 进程CLI 模式)或 MINERU_APISERVERHTTP 模式,默认 host.docker.internal:9987),后端可选 pipeline / vlm-http-client / vlm-transformers / vlm-vllm-engine;输出 zip 解压后融合为 sections + tables。naive.py:45-62
  • TextLnParserapp/textin_parser.py):合合 TextIn 云端 PDF→Markdown 服务,需要 TEXTLN_APP_ID/SECRET_CODE

5.3 Word 解析(deepdoc/parser/docx_parser.py + naive.py:Docx

两层:

  • 底层 RAGDocxParserdocx_parser.py:9-123python-docx+pandas 读段落与表格;表格内容经 __compose_table_content 做"列类型推断"(日期 Dt / 数字 Nu / 中文人名 Nr / 英文 En 等 11 类正则),自动识别多行表头并把单元格拼成 表头:值 格式,保证表格在 chunk 中也能被关键词检索。
  • 上层 Docx(RAGDocxParser)naive.py:105-323):把段落里的图片用 python-docxxpath('.//pic:pic') 抽出,挂到对应 paragraph表格区域用 __get_nearest_title 上溯到 7 级标题构造层级路径作为 <caption>Table Location: A > B > C</caption>,这是检索时定位表格上下文的关键。
  • 超链接抽取extract_links_from_docx 遍历 document.part.rels,过滤 reltype 为 hyperlink 的关系,得到链接集合。utils/file_utils.py:133-154
  • to_markdown:可选回退路径,使用 mammoth.convert_to_html + markdownify,图片嵌成 data: base64 URL。naive.py:325-366
  • NULL 关系修复:上层 Docxload_from_xml_v2 monkey-patch 掉 _SerializedRelationships.load_from_xml,跳过 ../NULLNULL target 以绕过 python-docx#1105 已知 bug。naive.py:493-506, 569

5.4 Excel/CSV 解析(deepdoc/parser/excel_parser.py

  • 多引擎容错_load_excel_to_workbook 先看魔数:PK\x03\x04OOXML\xd0\xcf\x11\xe0OLE2。openpyxl 失败回退 pandas.read_excel,再失败回退 engine="calamine";非 Excel 头则当 CSV 处理(pd.read_csv(on_bad_lines='skip') 容忍坏行)。excel_parser.py:18-53
  • 非法字符清洗ILLEGAL_CHARACTERS_RE = re.compile(r"[\000-\010]|[\013-\014]|[\016-\037]")_clean_dataframe 把所有字符串单元格里的控制字符替换成空格,避免写入 Workbook 报错。excel_parser.py:13, 56-62
  • 三种输出形态
    • __call__():每行 → 表头1值1\n表头2值2\n...\n——SheetName,作为一个 section一个 chunkexcel_parser.py:203-246
    • html():每 256 行打包成一张 <table>header 复用,便于检索时整块召回。excel_parser.py:144-187
    • markdown()df.to_markdown(index=False),整个表一段。
  • 图片抽取_extract_images_from_worksheet 通过 ws._images 的 anchor.row/col 还原图片位置,输出 single_cell / multi_cell span_type 元数据。excel_parser.py:98-142
  • 重要Excel 不走 naive_mergenaive.py:678-680 显式注释"Excel 每行直接作为一个 chunk不经过 naive_merge 避免被 delimiter 拆分"——直接 tokenize_chunks(chunks, ...)

5.5 Markdown 解析(deepdoc/parser/markdown_parser.py

  • 表格抽取extract_tables_and_remainder 用三个正则按顺序剥离:标准 GFM 边框表格 → 无边框表格 → HTML <table>...</table>(含 <html><body> 包装),每张表单独成一个 chunk剩余正文继续走 element 抽取。markdown_parser.py:10-106
  • 元素抽取MarkdownElementExtractor.extract_elements(delimiter) 按行扫描,识别 header(#~######) / code_block(```) / list_block(-/*/+/数字.) / blockquote(>) / text_block,每种元素用对应私有方法收集起止行号。markdown_parser.py:109-277
  • 图片嵌入:当传入 vision_modelnaive.py 会对每个 section 调 markdown_parser.get_pictures()HTTP 下载或本地路径打开),把图片合并 concat_img 后丢给 VisionFigureParser 让 LLM 描述图片,描述文本拼回 section 末尾。naive.py:697-709
  • 超链接深挖get_hyperlink_urls(soup) + analyze_hyperlink=True 触发递归 chunk。naive.py:716-720

5.6 HTML 解析(deepdoc/parser/html_parser.py

  • 预清洗BeautifulSoup html5lib移除 <style>/<script>,剥离 inline style 属性与 HTML 注释。html_parser.py:39-52
  • 递归读文本read_text_recursively 给每个 BLOCK_TAGh1-h6/p/div/article/section/aside/ul/ol/li/table/pre/code/blockquote/figure/figcaption分配 block_id UUID把 NavigableString 收集到所属 block。<table> 整段保留,单独给 table_id 元数据。html_parser.py:89-131
  • 标题前缀化merge_block_text 在拼接时把 h1-h6 改写为 # ~ ######Markdown 风格),保留层级语义到下游。html_parser.py:134-161
  • 二次切分chunk_block(block_txt_list, chunk_token_num=512)chunk_token_num(默认 512合并 block超长 block 用 rag_tokenizer.tokenize() 切成等长片段。html_parser.py:163-196

5.7 JSON 解析(deepdoc/parser/json_parser.py

  • 结构感知切分_json_split 递归遍历 dict_json_size(即 json.dumps(...) 的字符长度,乘以 2 作为 max_chunk_size累计超过 max 但当前 chunk ≥ minmax - 200)时开新 chunk否则继续递归到子节点。关键设计list 通过 _list_to_dict_preprocessing 转成索引化 dict让数组也能按结构切分。json_parser.py:46-95
  • JSONL 自动检测is_jsonl_format 抽样前 10 行,若 ≥ 80% 行单独 json.loads 成功且整体不能 parse 为单个 JSON则按 JSONL 处理。json_parser.py:134-152

5.8 TXT/代码 解析(deepdoc/parser/txt_parser.py

  • 简单版:get_text 读全文(find_codec 嗅探编码),按 delimiter="\n!?;。;!?" 切分,就地累加 token:当前 chunk 超 chunk_token_num 才开新 chunk。txt_parser.py:8-48
  • 适配的扩展名集:.txt|.py|.js|.java|.c|.cpp|.h|.php|.go|.ts|.sh|.cs|.kt|.sqlnaive.py:685

5.9 图片/音视频(app/picture.py / app/audio.py

  • 图片:from app.core.rag.app.picture import chunkpicture_vision_llm_chunk(binary, vision_model, prompt, callback),多模态 LLM 直接产文。
  • 音视频:from app.core.rag.app.audio import chunk → 调 seq2txt_mdlQWenSeq2txtqwen3-omni-flash)做语音转文字。
  • PDF 也可以走 VisionParser 让 LLM 整页"看图说话",是 OCR 失败/扫描件的兜底。

5.10 PPT 与 .doc外部依赖

  • PPTX/PPTnaive.py:628-651:调 async_convert_to_pdfutils/libre_office.py:59-62)把文件转 PDF再递归 chunk(dest_pdf_path, ...)
  • LibreOffice 路径硬编码 /usr/bin/sofficeLinux/Applications/LibreOffice.app/Contents/MacOS/sofficemacOS都不存在则抛 HTTP 500subprocess.runtimeout=120s 防卡死。utils/libre_office.py:11-57
  • ThreadPoolExecutor(max_workers=os.cpu_count()*2) 提交异步转换任务;同进程多请求共享线程池。
  • DOC旧版二进制naive.py:738-761:使用 Apache Tikatika-server.jar JVM 进程,端口 9998。环境必须有 Java 11+;初始化 tika.initVM()tika_parser.from_file(filename)['content']\n 切分。

5.11 视觉子系统(deepdoc/vision/

  • OCROCR.__call__(img, device_id, cls) 内部跑 TextDetector 检测文字框 → TextRecognizer 识别字符 → 可选方向分类。vision/ocr.py:522, 694。模型走 ONNX。
  • LayoutRecognizer4YOLOv10YOLOv10 ONNX 模型10 类 label__call__(image_list, ocr_res, scale_factor=3, thr=0.2, batch_size=16, drop=True) 中接收图像与 OCR 结果,输出每个文字框的 layout 类型并把 header/footer 等 drop 掉。
  • TableStructureRecognizer:检测表格单元格的列、行、列头、合并单元格等 5 类。
  • VisionFigureParserfigure_parser.py:52-118):用 ThreadPoolExecutor(10) 并发把每张图片喂给 vision_model,超时 30s@timeout(30, 3) 装饰器表示 30s 超时、3 次重试)。vision_llm_figure_describe_prompt() 给出统一的"详细描述这张图"指令。

6. Chunking 章节

6.1 Token 计数(common/token_utils.py

  • 模型固定为 tiktoken.cl100k_baseGPT-4 / text-embedding-ada-002 同口径),缓存目录 res/token_utils.py:6-9
  • num_tokens_from_string(s) 容错:encode 失败返回 0不会让上层报错token_utils.py:12-18
  • truncate(s, max_len) 按 token 截断,保护 LLM 上下文。token_utils.py:56-58

6.2 编码嗅探(nlp/__init__.py:37-55

  • find_codec(blob):先 chardet.detect(blob[:1024]) 置信度 > 0.5 用结果("ascii" 强制升级到 "utf-8",避开 chardet 经典误判);置信度低则按预设 80+ 编码顺序列表逐个 decode 尝试,全失败 fallback utf-8

6.3 主切分函数 naive_mergenlp/__init__.py:562-606

# api/app/core/rag/nlp/__init__.py:562-606核心算法
def naive_merge(sections, chunk_token_num=128, delimiter="\n。;!?", overlapped_percent=0):
    if isinstance(sections, str): sections = [sections]
    if isinstance(sections[0], str): sections = [(s, "") for s in sections]
    cks, tk_nums = [""], [0]
    def add_chunk(t, pos):
        nonlocal cks, tk_nums
        tnum = num_tokens_from_string(t)
        if tnum < 8: pos = ""        # 太短不挂位置
        if cks[-1] == "" or tk_nums[-1] > chunk_token_num * (100 - overlapped_percent)/100.:
            # 开新 chunk按 overlapped_percent 从上一块尾部留滑窗
            overlapped = RAGPdfParser.remove_tag(cks[-1])
            t = overlapped[int(len(overlapped)*(100-overlapped_percent)/100.):] + t
            if t.find(pos) < 0: t += pos
            cks.append(t); tk_nums.append(tnum)
        else:
            if cks[-1].find(pos) < 0: t += pos
            cks[-1] += t; tk_nums[-1] += tnum
    dels = get_delimiters(delimiter)
    for sec, pos in sections:
        if num_tokens_from_string(sec) < chunk_token_num:
            add_chunk("\n"+sec, pos); continue
        for sub_sec in re.split(r"(%s)" % dels, sec, flags=re.DOTALL):
            if re.match(f"^{dels}$", sub_sec): continue
            add_chunk("\n"+sub_sec, pos)
    return cks

要点:

  • token 上限:当 tk_nums[-1] > chunk_token_num * (1 - overlapped_percent/100) 时开新 chunk。这意味着 overlapped_percent=0 → 严格不超;>0 → 提前开新块以预留滑窗空间。
  • 滑动窗口:开新 chunk 时把上一块尾部 overlapped_percent% 的字符(不是 token)拼到新块开头;用 RAGPdfParser.remove_tag 先剥离位置标签,避免位置 tag 漏到新块。
  • delimiter:默认 "\n。",可被 parser_config.delimiter 覆盖。get_delimiters 支持反引号包围的多字符分隔符(如 `\n\n`),并按长度降序优先匹配(避免短符号"吞掉"长符号的左边界)。nlp/__init__.py:760-776
  • 位置 tag 注入:每段 pos 串只在 chunk 内不存在时才追加避免重复PDF chunk 一段往往跨多页,位置 tag 自然多次出现)。
  • 长 section 二次拆分:单段 token 数 ≥ chunk_token_num 才用 delimiter 切,否则整段加入。

6.4 带图变体

  • naive_merge_docxnlp/__init__.py:706-752sections 是 [(text, image), ...];无图段先累积成行 line遇到带图段才触发切分同一 chunk 内多图用 concat_img 上下拼接成一张大图。没有 overlapped_percent
  • naive_merge_with_imagesnlp/__init__.py:609-662):与 naive_merge 同构,但同步把每段对应的 image 累积到 result_images 数组(多图也走 concat_img 合并)。

6.5 标题树切分(结构感知)

  • hierarchical_merge(bull, sections, depth)nlp/__init__.py:471-559):用 BULLET_PATTERN[bull]5 套样式:第一/二/三章节系列、英文 PART/Chapter、Markdown # 系列)匹配标题,按层级建组,每组内累计 token 不超过 218 就合并到一个 chunk。是 manual.py / paper.py 等"标准化文档"app 用的策略。
  • tree_merge(bull, sections, depth)nlp/__init__.py:423-469):同样基于 BULLET_PATTERN但建一棵 Node 标题树,深度优先生成 chunk让父级标题路径自动出现在每个 chunk 头部(title1\ntitle2\nbody)。
  • 这两个函数 不在 app/naive.py 主链路调用——naive.py 用的是 naive_merge 系列;它们服务于 app/manual.pyapp/paper.pyapp/laws.pyapp/book.py 等专业 app。

6.6 关键词处理(tokenize / tokenize_chunks / tokenize_table

最终交给 ES 的不是裸文本 chunk而是带"分词字段"的 doc

# api/app/core/rag/nlp/__init__.py:251-256
def tokenize(d, t, eng):
    d["content_with_weight"] = t
    t = re.sub(r"</?(table|td|caption|tr|th)( [^<>]{0,12})?>", " ", t)
    d["content_ltks"] = rag_tokenizer.tokenize(t)               # 粗粒度分词
    d["content_sm_ltks"] = rag_tokenizer.fine_grained_tokenize(d["content_ltks"])  # 细粒度
# api/app/core/rag/nlp/__init__.py:258-277tokenize_chunks
for ii, ck in enumerate(chunks):
    d = copy.deepcopy(doc)                                  # doc 含 docnm_kwd / title_tks / title_sm_tks
    if pdf_parser:                                          # 仅 PDF 链路
        d["image"], poss = pdf_parser.crop(ck, need_position=True)
        add_positions(d, poss)
        ck = pdf_parser.remove_tag(ck)
    else:
        add_positions(d, [[ii]*5])                          # 无位置时填占位
    tokenize(d, ck, eng)
    res.append(d)
  • add_positions(d, poss) 写入 page_num_int / position_int / top_int 三列(int 后缀是 ES 的 type hintnlp/__init__.py:325-337
  • tokenize_table(tbls, doc, eng, batch_size=10) 每 10 行表格组装成一个 chunk挂图如有doc_type_kwd="image"nlp/__init__.py:295-322

6.7 Chunk Pydantic 模型(models/chunk.py

# api/app/core/rag/models/chunk.py
class ChildDocumentChunk(BaseModel):
    page_content: str
    vector: list[float] | None = None
    metadata: dict = Field(default_factory=dict)

class DocumentChunk(BaseModel):           # 父子结构
    page_content: str
    vector: list[float] | None = None
    metadata: dict = Field(default_factory=dict)
    children: list[ChildDocumentChunk] | None = None

class GeneralStructureChunk(BaseModel):
    general_chunks: list[str]

class ParentChildChunk(BaseModel):
    parent_content: str
    child_contents: list[str]

class ParentChildStructureChunk(BaseModel):
    parent_child_chunks: list[ParentChildChunk]
    parent_mode: str = "paragraph"        # 父分段模式

class QAChunk(BaseModel):
    question: str
    answer: str

class QAStructureChunk(BaseModel):
    qa_chunks: list[QAChunk]

重要:DocumentChunk 是上层服务(services/controllers/chunk_controller.py)使用的"业务 schema",与 tokenize_chunks 输出的 ES doc 字段不同。 ES doc 实际字段(来自 nlp/__init__.py 注入):

  • docnm_kwd原文件名keyword
  • title_tks / title_sm_tks:标题分词(粗 + 细)
  • content_with_weight:原始 chunk 文本(用于 BM25 加权)
  • content_ltks / content_sm_ltks:内容分词(粗 + 细)
  • page_num_int / position_int / top_int:页码与坐标(用于 PDF 还原图片)
  • imagePIL.Image存为二进制
  • doc_type_kwddoc 类型("image" / 默认空)
  • 后续 Embedding 阶段补 q_vec_<dim>(向量字段,详见 [S2-T2])。

6.8 切分策略汇总

策略 实现 默认参数 触发条件
按 Token + delimiter(默认) naive_merge chunk_token_num=128/512, delimiter="\n!?。;!?" docx/pdf/html/json/md/txt主链路
滑动窗口 naive_mergeoverlapped_percent 默认 0 parser_config.overlapped_percent=N(手动)
按行(无合并) naive.py:678-680 excel_parser 每行一段 xlsx/csv
按段落 + 图绑定 naive_merge_docx 同 naive_merge docx
按段落 + 多模态 naive_merge_with_images 同 naive_merge md含图/ pdf VisionParser
结构化 JSON 切分 RAGJsonParser._json_split max_chunk_size=4000 chars, min_chunk_size=max-200 json/jsonl/ldjson
按 token 切分HTML block RAGHtmlParser.chunk_block chunk_token_num=512 html
基于标题树 hierarchical_merge / tree_merge depth 参数token 上限硬编码 218 manual/paper/book/laws app
整段(不切) tokenize_chunks 直接喂 chunks mineru/textln内置已切好

7. 关键源码片段速查

文件 行号 内容
api/app/core/rag/app/naive.py 27-95 by_deepdoc / by_mineru / by_textln / by_plaintext 四个 PDF 适配器
api/app/core/rag/app/naive.py 97-102 PARSERS 注册表
api/app/core/rag/app/naive.py 369-412 class Pdf(PdfParser)OCR→layout→TSR→merge 编排
api/app/core/rag/app/naive.py 508-811 def chunk(...):所有格式的总入口
api/app/core/rag/nlp/__init__.py 562-606 naive_merge(主切分)
api/app/core/rag/nlp/__init__.py 706-752 naive_merge_docx(图绑定)
api/app/core/rag/nlp/__init__.py 251-256 tokenize(生成分词字段)
api/app/core/rag/nlp/__init__.py 258-277 tokenize_chunksPDF 裁图 + 位置)
api/app/core/rag/nlp/__init__.py 295-322 tokenize_table(表格 batch=10
api/app/core/rag/nlp/__init__.py 152-184 BULLET_PATTERN5 套标题样式)
api/app/core/rag/common/token_utils.py 6-18 tiktoken.cl100k_base + num_tokens_from_string
api/app/core/rag/crawler/web_crawler.py 81-183 WebCrawler.crawl() 主循环
api/app/core/rag/crawler/http_fetcher.py 42-180 HTTPFetcher.fetch 重试/退避/4xx/5xx 处理
api/app/core/rag/integrations/feishu/client.py 68-127 tenant_access_token + TTLCache + asyncio.Lock
api/app/core/rag/integrations/feishu/client.py 311-406 _export_file 三步轮询导出
api/app/core/rag/integrations/yuque/client.py 234-291 get_doc_detail(raw=1)
api/app/core/rag/integrations/yuque/client.py 364-455 lakesheet → xlsx 重建
api/app/core/rag/utils/libre_office.py 11-57 convert_to_pdf 软件路径 + 120s 超时
api/app/core/rag/utils/file_utils.py 69-130 extract_embed_filezip/OLE 双路径)
api/app/core/rag/deepdoc/parser/pdf_parser.py 1006-1122 __images__ OCR 主入口trio 并发)
api/app/core/rag/deepdoc/parser/pdf_parser.py 1219-1229 remove_tag / extract_positions
api/app/core/rag/deepdoc/parser/pdf_parser.py 1300-1331 PlainParserpypdf 兜底)
api/app/core/rag/deepdoc/parser/pdf_parser.py 1334-1383 VisionParser(多模态 LLM 整页)
api/app/core/rag/deepdoc/parser/excel_parser.py 18-53 _load_excel_to_workbookopenpyxl/pandas/calamine 三级回退)
api/app/core/rag/deepdoc/parser/json_parser.py 46-95 _json_split 结构感知切分
api/app/core/rag/deepdoc/parser/figure_parser.py 52-118 VisionFigureParser10 并发 LLM 描述图)
api/app/core/rag/deepdoc/vision/layout_recognizer.py 147-160 YOLOv10 10 类 label

8. 配置项与可调参数

8.1 parser_confignaive.py:521 默认值,业务侧可覆盖)

参数 默认 含义 影响
layout_recognize "DeepDOC" PDF 引擎选择 DeepDOC/Plain Text/MinerU/TextLn
chunk_token_num 512PDF 默认)/ 128(其他默认) 单 chunk 最大 token 直接影响召回粒度与上下文密度
delimiter "\n!?。;!?" 切分分隔符(支持反引号多字符) 细化语义边界
analyze_hyperlink True 是否递归抓 docx/pdf 内超链接 显著拉长解析时间
html4excel "false" Excel 是否走 HTML 表格输出 表格检索友好度 vs token 浪费
auto_keywords 0 自动关键词提取数 下游 prompt 注入
auto_questions 0 自动问题提取数 QA-RAG
overlapped_percent 0 滑窗重叠百分比 召回连续性 vs 冗余

8.2 环境变量

变量 默认 用途
LAYOUT_RECOGNIZER_TYPE onnx onnx / ascend 切换 NPU
HF_ENDPOINT https://hf-mirror.com 加速国内 HF 拉取
MINERU_EXECUTABLE mineru MinerU CLI 路径
MINERU_APISERVER http://host.docker.internal:9987 MinerU HTTP API
MINERU_BACKEND pipeline pipeline / vlm-http-client / vlm-transformers / vlm-vllm-engine
MINERU_DELETE_OUTPUT 1 是否清理临时输出
TEXTLN_APISERVER https://api.textin.com/... TextIn 云端
TEXTLN_APP_ID/SECRET_CODE TextIn 鉴权
TIKA_SERVER_JAR /tmp/tika-server.jar Apache Tika jar 路径
TIKA_SERVER_PORT 9998 Tika JVM 端口

8.3 调用入参(chunk() 形参)

参数 含义
filename / binary 文件路径或二进制内容(推荐 binarypath 模式不支持嵌入抽取)
from_page / to_page PDF 分页范围(节省内存)
lang "Chinese" / "english"(影响 is_english 与表格分隔符)
vision_model 多模态 LLM 实例图片描述、VisionParser、音视频
pdf_cls 自定义 PDF 类,继承 Pdf(可重写 OCR/layout 钩子)
is_root 内部递归标志,外部勿设
section_only 仅返回切分文本,不做 ES doc 包装(用于增量调试)

9. 边界条件与已知限制

  1. PPT/DOC 强依赖外部组件LibreOffice 与 Apache Tika 任一缺失都会让对应格式直接 500没有内建兜底。建议生产容器固化版本。
  2. extract_embed_file 不支持 path 模式:仅接受 bytesroot 调用必须传 binary 否则抛 Exceptionnaive.py:541)。
  3. HF 模型懒加载:首次启动会从 HuggingFace 拉 text_concat_xgb_v1.0 与 OCR/layout/TSR 模型(共数百 MB冷启动慢建议 image build 阶段预热。
  4. 同进程 PDF 锁LOCK_KEY_pdfplumber 全局 lock 串行化 pdfplumber.open()单进程内 PDF 解析无法真并发;需要并发则起多进程或多容器。
  5. naive_merge 滑窗按字符不按 tokenoverlapped_percent=20 实际重叠是上一块字符串末尾 20% 字符token 数会有偏差(中文字符占 1-3 token 不等)。
  6. 图片 chunk 无 position_inttokenize_chunks_with_images 只填了 [ii]*5(占位),不能像 PDF chunk 那样在原图上还原坐标。
  7. naive_merge_docx 没有 overlapped_percentdocx 链路无重叠窗口(实现上漏掉了),如需重叠暂时只能改代码或者把 docx → markdown 再走 markdown 链路。
  8. JSONL 检测启发式is_jsonl_format 只看前 10 行 80% 阈值,对"前几行恰好都是合法单行 JSON 但整体也是合法 JSON 数组"的边界情况会误判。
  9. Crawler 不支持 SPAis_static_content 直接拒绝 <200 chars body + >5 scripts 的页面,没有 Playwright/Puppeteer 渲染兜底。
  10. 飞书在线文档导出超时_export_file 写死 max_retries=10, poll_interval=2s(即 20s 上限),大文档可能超时 → FeishuAPIError("Export task did not complete...")

10. 监控指标与排错指引

10.1 关键日志锚点(按 timer 输出)

  • __images__ dedupe_chars cost {t}sPDF 字符抽取)
  • __images__ {N} pages cost {t}sOCR 总耗时)
  • naive_merge({filename}): {t}chunking 耗时)
  • OCR finished / Layout analysis / Table analysis / Text mergedcallback 进度)

10.2 常见 Bug & 定位

现象 可能原因 定位
中文 chunk 出现乱码 文件编码非 UTF-8 但 find_codec 误判 find_codec 入口打日志看 chardet.detect 返回
Excel 单元格丢失 ILLEGAL_CHARACTERS_RE 把控制字符替换成空格 _clean_dataframe 是不是把业务字符当成非法字符了
PDF 图被截到一半 crop() 计算 bottom 时跨页页高累积出错 pdf_parser.py:1245-1260 检查 page_cum_height
飞书 token 频繁刷新 TTLCache(ttl=7200-300) 只缓存 1 token多并发实例每个进程独立缓存 接 Redis 共享缓存
MinerU 报"not found" MINERU_EXECUTABLE PATH 不对 mineru_parser.py:check_installation 打 trace
chunk 数远超预期 chunk_token_num 太小 + delimiter 过细 naive_merge 入口的两个参数
解析卡死无反应 LibreOffice 转换卡 / Tika JVM 挂 检查 convert_to_pdf 的 120s timeout 是否触发
HF 模型拉取失败 国内网络 export HF_ENDPOINT=https://hf-mirror.com

11. 优化建议与未来扩展点

11.1 架构改造建议(即刻收益)

  1. Loader 抽象层:把 WebCrawler / FeishuAPIClient / YuqueAPIClient 统一收敛为 BaseLoader.load() -> Iterable[LoadedDocument] 接口,下游统一消费 LoadedDocument(filename, binary, source_metadata)。这样 confluence/Notion/SharePoint 接入只需新写一个 Loader不用改 naive.py
  2. Parser 注册表外露PARSERS = {...} 当前只覆盖 PDF建议扩到 FORMAT_PARSERS = {".docx": Docx, ".xlsx": ExcelParser, ...},把 chunk() 里的大 if-elif 链替换成 dict 查表 + 插件机制。新格式(如 epub/odt通过 register_parser(".epub", EpubParser) 注入。
  3. Chunking 策略策略化:把 naive_merge / naive_merge_docx / naive_merge_with_images / hierarchical_merge / tree_merge 实现 BaseChunker 接口(chunk(sections) -> List[Chunk]),由 parser_config.chunking_strategy 选择。当前 docx 缺 overlapped_percent 这种"碎片化丢失"会自然消失。
  4. Token 切分而非字符切分naive_merge 的滑窗用 encoder.encode(text)[-N:] 反解 token-level overlap避免中文字符≠token 的口径错配。
  5. 共享 token 缓存:飞书/语雀 token 改为 Redis 共享,目前每实例一份的 TTLCache 在 K8s 多副本下会触发限流。
  6. 嵌入文件深度限制extract_embed_file 是"only first layer",但调用方递归 chunk(...is_root=False) 没有 depth guard恶意文件可造成栈深递归 → 加 max_depth=3
  7. PDF 解析进程化pdfplumber 全局锁实质单线程,对 PDF 重负载场景把 Pdf 包成独立 workermultiprocessing 或 ProcessPoolExecutor让 OCR/layout 跨核并行。

11.2 功能扩展方向

  1. 多模态深整合:现在 VisionParser / picture_vision_llm_chunk / VisionFigureParser 都是"图 → 描述文本 → 文本 chunk"的有损转换;可以保留 image embedding 与 text embedding 并存下游做多模态混合检索CLIP/SigLIP 与文本向量并列召回)。
  2. 语义切分Semantic Chunking:按嵌入相似度(如 cosine(emb_i, emb_{i+1}) < 0.7 作为切点)替代固定 token 切分,实验证明可显著提升长文档召回。naive_merge 已经有插槽,加一个 chunking_strategy="semantic" 即可。
  3. 结构化字段抽取:现在表格只做行→自然语言转换("列名:值"),没有把表格存成结构化 JSON。可在 tokenize_table 旁路输出 table_data: dict,配合 [S2-T3] 的混合搜索,用关键词字段精确过滤。
  4. 缓存命中:相同文件的解析结果(按 sha256 + parser_config hash应进缓存重新入库时跳过 OCRextract_embed_file 已有 _sha10 雏形,可扩为完整 cache key。
  5. 流式 chunk 输出:当前 chunk() 返回 List,大文件全量加载到内存;改为 Iterable[Chunk] + 生产者-消费者,可以让 Embedding 与 OCR 并行流水线。
  6. 更细粒度的进度上报callback(prog, msg) 现在是粗粒度0.1/0.6/0.8…),生产中需要展示"第几页/共多少页",建议结构化为 callback({stage, current, total, msg})
  7. Crawler 增量化:当前每次全量 crawl没有 ETag/If-Modified-Since 机制;接 last_crawl_timestamp 让二次抓取只拉变化页。

11.3 与下游约定(输出契约)

本文档负责输出的 chunk 序列应包含至少:

{
    "docnm_kwd": str,                 # 文件名
    "title_tks": str,                 # 文档标题分词(粗)
    "title_sm_tks": str,              # 文档标题分词(细)
    "content_with_weight": str,       # 原始 chunk 文本(必填)
    "content_ltks": str,              # 内容分词(粗)
    "content_sm_ltks": str,           # 内容分词(细)
    "page_num_int": list[int],        # 页码PDF 才有意义)
    "position_int": list[tuple],      # (page, x0, x1, top, bottom)
    "top_int": list[int],             # 行顶 y 坐标
    "image": Optional[PIL.Image],     # PDF/Excel 才有
    "doc_type_kwd": Optional[str],    # "image" 或空
}

[S2-T3] 索引结构应消费上述字段(参考 vdb/elasticsearch/elasticsearch_vector.py 的 mapping。[S2-T2] Embedding 应在此基础上补 q_<dim>_vec 列。[S2-T6] 端到端调用链路从 app/naive.py:chunk() 开始追踪。


自检清单(对照 [S1-T1] 评分卡,预估 ≥ 80

  • 准确性:所有源码引用经 grep 与 line read 验证,路径/函数名/行号 ±3 行内
  • 完整性:覆盖 Loader4 种)/ Parser11 种格式)/ Chunking8 种策略)/ Chunk 模型 / 配置项 / 限制 / 排错
  • 时效性:基于 origin/main HEAD2026-05-08
  • 可读性分层目录、表格、Mermaid 图、源码片段交叉
  • 可执行性:环境变量、参数默认值、外部依赖列出可直接落地