md-rag-core 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. md_rag_core-0.0.1/.gitignore +58 -0
  2. md_rag_core-0.0.1/PKG-INFO +18 -0
  3. md_rag_core-0.0.1/README.md +0 -0
  4. md_rag_core-0.0.1/pyproject.toml +63 -0
  5. md_rag_core-0.0.1/src/md_rag_core/__init__.py +1 -0
  6. md_rag_core-0.0.1/src/md_rag_core/chunker/__init__.py +25 -0
  7. md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/__init__.py +0 -0
  8. md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/base.py +194 -0
  9. md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/code_aggregator.py +241 -0
  10. md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/image_aggregator.py +95 -0
  11. md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/list_aggregator.py +227 -0
  12. md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/math_aggregator.py +90 -0
  13. md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/section_aggregator.py +154 -0
  14. md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/table_aggregator.py +308 -0
  15. md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/text_aggregator.py +95 -0
  16. md_rag_core-0.0.1/src/md_rag_core/chunker/builder.py +131 -0
  17. md_rag_core-0.0.1/src/md_rag_core/chunker/models.py +240 -0
  18. md_rag_core-0.0.1/src/md_rag_core/chunker/post_processor.py +319 -0
  19. md_rag_core-0.0.1/src/md_rag_core/chunker/section_splitter.py +83 -0
  20. md_rag_core-0.0.1/src/md_rag_core/parser/__init__.py +69 -0
  21. md_rag_core-0.0.1/src/md_rag_core/parser/handlers/__init__.py +65 -0
  22. md_rag_core-0.0.1/src/md_rag_core/parser/handlers/base.py +79 -0
  23. md_rag_core-0.0.1/src/md_rag_core/parser/handlers/code_handler.py +82 -0
  24. md_rag_core-0.0.1/src/md_rag_core/parser/handlers/heading_handler.py +114 -0
  25. md_rag_core-0.0.1/src/md_rag_core/parser/handlers/html_table_handler.py +157 -0
  26. md_rag_core-0.0.1/src/md_rag_core/parser/handlers/image_handler.py +67 -0
  27. md_rag_core-0.0.1/src/md_rag_core/parser/handlers/list_handler.py +167 -0
  28. md_rag_core-0.0.1/src/md_rag_core/parser/handlers/math_handler.py +195 -0
  29. md_rag_core-0.0.1/src/md_rag_core/parser/handlers/md_table_handler.py +186 -0
  30. md_rag_core-0.0.1/src/md_rag_core/parser/inline_parser.py +197 -0
  31. md_rag_core-0.0.1/src/md_rag_core/parser/models.py +188 -0
  32. md_rag_core-0.0.1/src/md_rag_core/parser/parser.py +156 -0
@@ -0,0 +1,58 @@
1
+ # Compiled class file
2
+ *.class
3
+
4
+ # Log file
5
+ *.log
6
+
7
+ # BlueJ files
8
+ *.ctxt
9
+
10
+ *.ini
11
+
12
+ # Mobile Tools for Java (J2ME)
13
+ .mtj.tmp/
14
+
15
+ # Package Files #
16
+ *.jar
17
+ *.war
18
+
19
+ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
20
+ hs_err_pid*
21
+
22
+ # IDEA or eclipse files #
23
+ .settings/
24
+ .project
25
+ .classpath
26
+ .factorypath
27
+ .idea/
28
+ *.iml
29
+
30
+ # mvn target
31
+ target/
32
+ .mvn/
33
+
34
+ # log files
35
+ logs/
36
+
37
+ # npm files
38
+ node_modules/
39
+ rebel.xml
40
+
41
+ # mac os
42
+ .DS_Store
43
+
44
+ docker-compose/
45
+ finalshell/
46
+ __pycache__/
47
+ nlb-nginx/
48
+ build/
49
+ dist/
50
+ .pytest_cache/
51
+ /royal_tsx/
52
+ deploy_python/
53
+ inis-*/
54
+ logs/
55
+ *.c
56
+ *.key
57
+ *.pem
58
+ nacos-data
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: md-rag-core
3
+ Version: 0.0.1
4
+ Summary: A core library for Markdown-aware chunking, semantic structuring, vectorization, and RAG retrieval.
5
+ Author-email: ferdinand <gongjpa@gmail.com>
6
+ License-Expression: Apache-2.0 OR MIT
7
+ Keywords: RAG,chunking,markdown,retrieval,semantic
8
+ Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.12
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest>=9.0.2; extra == 'test'
File without changes
@@ -0,0 +1,63 @@
1
+ [project]
2
+ name = "md-rag-core"
3
+ dynamic = ["version"]
4
+ description = "A core library for Markdown-aware chunking, semantic structuring, vectorization, and RAG retrieval."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = []
8
+
9
+ # 协议信息
10
+ license = "Apache-2.0 OR MIT"
11
+ authors = [
12
+ { name = "ferdinand", email = "gongjpa@gmail.com" },
13
+ ]
14
+
15
+ # 可以到网站https://pypi.org/classifiers/查找合适的classifiers
16
+ classifiers = [
17
+ "Development Status :: 2 - Pre-Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: Apache Software License",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Software Development :: Libraries :: Python Modules",
25
+ ]
26
+
27
+ # 关键字方便快速检索
28
+ keywords = [
29
+ "markdown", "semantic", "RAG", "retrieval", "chunking"
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ test = [
34
+ "pytest>=9.0.2",
35
+ ]
36
+
37
+ [build-system]
38
+ requires = ["hatchling"]
39
+ build-backend = "hatchling.build"
40
+
41
+
42
+ # === 关键配置: 指定包的位置 ===
43
+ # 告诉 hatchling,真正得包代码在 src 目录下
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/md_rag_core"]
46
+
47
+ [tool.hatch.build.targets.sdist]
48
+ # 明确指定 sdist 只包含 src 目录,排除 test 目录等其他根目录文件
49
+ include = [
50
+ "src",
51
+ # 建议也包含 README 和 LICENSE,否则 pip 安装有时会发出警告
52
+ "README.md",
53
+ "pyproject.toml"
54
+ ]
55
+
56
+ [tool.hatch.version]
57
+ path = "src/md_rag_core/__init__.py"
58
+
59
+ [[tool.uv.index]]
60
+ name = "testpypi"
61
+ url = "https://test.pypi.org/simple/"
62
+ publish-url = "https://test.pypi.org/legacy/"
63
+ explicit = true
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
@@ -0,0 +1,25 @@
1
+ """
2
+ chunker/aggregators/__init__.py — 聚合器包导出
3
+
4
+ 统一导出所有 Aggregator 类,方便外部直接导入。
5
+ """
6
+
7
+ from .aggregators.base import BaseAggregator
8
+ from .aggregators.text_aggregator import TextAggregator
9
+ from .aggregators.code_aggregator import CodeAggregator
10
+ from .aggregators.table_aggregator import TableAggregator
11
+ from .aggregators.math_aggregator import MathAggregator
12
+ from .aggregators.list_aggregator import ListAggregator
13
+ from .aggregators.image_aggregator import ImageAggregator
14
+ from .aggregators.section_aggregator import SectionAggregator
15
+
16
+ __all__ = [
17
+ "BaseAggregator",
18
+ "TextAggregator",
19
+ "CodeAggregator",
20
+ "TableAggregator",
21
+ "MathAggregator",
22
+ "ListAggregator",
23
+ "ImageAggregator",
24
+ "SectionAggregator",
25
+ ]
@@ -0,0 +1,194 @@
1
+ """
2
+ chunker/aggregators/base.py — 聚合器抽象基类
3
+
4
+ 定义所有类型感知聚合器(Aggregator)的统一接口。
5
+
6
+ 设计思路
7
+ --------
8
+ 与 Block 解析层的 Handler 模式类似,Chunk 层的聚合也采用"策略模式":
9
+ 每种 Block 类型对应一个独立的 Aggregator,负责将该类型的 Block 组合成 Chunk。
10
+
11
+ 主协调器(SectionAggregator)按类型将 Block 分流给对应的 Aggregator,
12
+ 每个 Aggregator 只关心自己负责的类型,互不干扰,便于独立测试和扩展。
13
+
14
+ Aggregator 的职责
15
+ -----------------
16
+ 1. 接收一组同类 Block(及其位置上下文)
17
+ 2. 按本类型的语义聚合规则,生成一个或多个原始 Chunk(尚未注入面包屑)
18
+ 3. 返回 Chunk 列表
19
+
20
+ 注意:Aggregator 不负责注入标题面包屑,也不负责 token 大小的最终校正,
21
+ 这两项工作由后处理器(PostProcessor)统一完成。
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from abc import ABC, abstractmethod
27
+ from typing import Any
28
+
29
+ from ...chunker.models import Chunk, ChunkConfig, Section
30
+
31
+
32
+ class BaseAggregator(ABC):
33
+ """
34
+ 聚合器抽象基类。
35
+
36
+ 子类需实现 aggregate() 方法,将 Section 内的一组 Block 组合为 Chunk 列表。
37
+ """
38
+
39
+ def __init__(self, config: ChunkConfig) -> None:
40
+ """
41
+ Args:
42
+ config : 全局拆分配置,控制 token 上限、上下文长度等参数
43
+ """
44
+ self.config = config
45
+
46
+ @abstractmethod
47
+ def aggregate(
48
+ self,
49
+ section: Section,
50
+ blocks: list[dict[str, Any]],
51
+ all_section_blocks: list[dict[str, Any]],
52
+ ) -> list[Chunk]:
53
+ """
54
+ 将一批 Block 聚合为 Chunk 列表。
55
+
56
+ Args:
57
+ section : 当前 Block 所属的 Section(用于获取 title_path)
58
+ blocks : 本次需要聚合的 Block 列表(已按类型筛选)
59
+ all_section_blocks : Section 内的全部 Block(用于查找上下文:
60
+ 代码/表格/公式需要取前后的 text Block)
61
+
62
+ Returns:
63
+ 生成的 Chunk 列表(已设置 content、chunk_type、source_blocks、token_count,
64
+ 但尚未注入 heading_prefix)
65
+ """
66
+ ...
67
+
68
+ # ------------------------------------------------------------------
69
+ # 工具方法(供子类共享)
70
+ # ------------------------------------------------------------------
71
+
72
+ def _find_prev_text_block(
73
+ self,
74
+ target_block: dict[str, Any],
75
+ all_blocks: list[dict[str, Any]],
76
+ max_tokens: int,
77
+ ) -> dict[str, Any] | None:
78
+ """
79
+ 在 all_blocks 中找到 target_block 之前最近的一个 text Block,
80
+ 且其 token 数不超过 max_tokens。
81
+
82
+ 用于代码/表格/公式聚合时绑定"前置说明段落"。
83
+
84
+ Args:
85
+ target_block : 目标 Block(代码/表格/公式)
86
+ all_blocks : Section 内的完整 Block 列表
87
+ max_tokens : 上下文 token 上限
88
+
89
+ Returns:
90
+ 符合条件的 text Block,或 None(不存在时)
91
+ """
92
+ from ...chunker.models import estimate_tokens
93
+
94
+ target_idx = self._find_block_position(target_block, all_blocks)
95
+ if target_idx <= 0:
96
+ return None
97
+
98
+ # 向前扫描,找第一个 text 类型的 Block
99
+ for i in range(target_idx - 1, -1, -1):
100
+ b = all_blocks[i]
101
+ if b.get("type") == "text":
102
+ if estimate_tokens(b.get("text", "")) <= max_tokens:
103
+ return b
104
+ # 超过 token 限制:截取前 max_tokens 对应的字符数
105
+ return self._truncate_block(b, max_tokens)
106
+ return None
107
+
108
+ def _find_next_text_block(
109
+ self,
110
+ target_block: dict[str, Any],
111
+ all_blocks: list[dict[str, Any]],
112
+ max_tokens: int,
113
+ ) -> dict[str, Any] | None:
114
+ """
115
+ 在 all_blocks 中找到 target_block 之后最近的一个 text Block,
116
+ 且其 token 数不超过 max_tokens。
117
+
118
+ 用于代码/表格/公式聚合时绑定"后置补充段落"。
119
+ """
120
+ from ...chunker.models import estimate_tokens
121
+
122
+ target_idx = self._find_block_position(target_block, all_blocks)
123
+ if target_idx < 0 or target_idx >= len(all_blocks) - 1:
124
+ return None
125
+
126
+ for i in range(target_idx + 1, len(all_blocks)):
127
+ b = all_blocks[i]
128
+ if b.get("type") == "text":
129
+ if estimate_tokens(b.get("text", "")) <= max_tokens:
130
+ return b
131
+ return self._truncate_block(b, max_tokens)
132
+ return None
133
+
134
+ @staticmethod
135
+ def _find_block_position(
136
+ target: dict[str, Any],
137
+ block_list: list[dict[str, Any]],
138
+ ) -> int:
139
+ """
140
+ 在 block_list 中定位 target Block 的索引。
141
+
142
+ 优先通过 _block_index 字段(全局原始索引)匹配,
143
+ 以应对 Block 字典被复制或部分修改的情况。
144
+
145
+ Args:
146
+ target : 目标 Block
147
+ block_list : 待搜索的列表
148
+
149
+ Returns:
150
+ 目标在列表中的位置索引,未找到返回 -1
151
+ """
152
+ target_global_idx = target.get("_block_index")
153
+ if target_global_idx is not None:
154
+ for i, b in enumerate(block_list):
155
+ if b.get("_block_index") == target_global_idx:
156
+ return i
157
+ # 降级:按对象 id 匹配
158
+ for i, b in enumerate(block_list):
159
+ if b is target:
160
+ return i
161
+ return -1
162
+
163
+ @staticmethod
164
+ def _truncate_block(block: dict[str, Any], max_tokens: int) -> dict[str, Any]:
165
+ """
166
+ 返回一个 text Block 的截断副本,使其 token 数不超过 max_tokens。
167
+
168
+ 截断在字符层面进行(chars ≈ tokens * 1.5),
169
+ 尽量在句子末尾(句号/换行)处截断,保证截断后语义相对完整。
170
+
171
+ Args:
172
+ block : 原始 text Block
173
+ max_tokens : 截断后的 token 上限
174
+
175
+ Returns:
176
+ 截断后的 Block 副本(不修改原始 Block)
177
+ """
178
+ max_chars = int(max_tokens * 1.5)
179
+ text = block.get("text", "")
180
+ if len(text) <= max_chars:
181
+ return block
182
+
183
+ # 在 max_chars 附近找最近的句子边界(句号、问号、感叹号、换行)
184
+ truncated = text[:max_chars]
185
+ for sep in ("。", "\n", ".", "!", "?", ";"):
186
+ pos = truncated.rfind(sep)
187
+ if pos > max_chars // 2: # 避免截断点太靠前
188
+ truncated = truncated[: pos + 1]
189
+ break
190
+
191
+ result = dict(block)
192
+ result["text"] = truncated
193
+ result["_truncated"] = True
194
+ return result
@@ -0,0 +1,241 @@
1
+ """
2
+ chunker/aggregators/code_aggregator.py — 代码块聚合器
3
+
4
+ 负责将代码 Block 与其上下文绑定,生成语义完整的 Chunk。
5
+
6
+ 问题背景
7
+ --------
8
+ 孤立的代码块向量无法表达"这段代码是干什么用的"。
9
+ 用户提问"怎么初始化连接池"时,代码块本身可能不含"连接池"这个词,
10
+ 无法被向量检索命中。
11
+
12
+ 绑定策略
13
+ --------
14
+ 前置上下文(≤ code_context_max token)
15
+ + 代码块完整内容
16
+ + 后置上下文(≤ code_context_max token,可选)
17
+ → 一个 code 类 Chunk
18
+
19
+ 超大代码块处理
20
+ --------------
21
+ 当代码块本身 token 数超过 code_token_max 时,按以下规则拆分:
22
+ 1. 按连续空行(≥2 个空行,通常是函数/类之间的间隔)拆分为逻辑段
23
+ 2. 每段保留代码语言标识,前置上下文只附加到第一段
24
+ 3. 相邻段之间保留 overlap_token_size 个 token 的重叠
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import re
30
+ from typing import Any
31
+
32
+ from ...chunker.models import Chunk, ChunkType, Section, estimate_tokens
33
+ from ...chunker.aggregators.base import BaseAggregator
34
+
35
+
36
+ class CodeAggregator(BaseAggregator):
37
+ """代码块聚合器,将代码 Block 与前后上下文绑定成 Chunk。"""
38
+
39
+ def aggregate(
40
+ self,
41
+ section: Section,
42
+ blocks: list[dict[str, Any]],
43
+ all_section_blocks: list[dict[str, Any]],
44
+ ) -> list[Chunk]:
45
+ """
46
+ 每个代码 Block 独立处理,绑定前后最近的 text Block 作为上下文。
47
+
48
+ Args:
49
+ section : 当前章节(提供 title_path)
50
+ blocks : 经过类型筛选的 code Block 列表
51
+ all_section_blocks : 章节内全部 Block(用于查找前后上下文)
52
+
53
+ Returns:
54
+ code 类 Chunk 列表
55
+ """
56
+ chunks: list[Chunk] = []
57
+
58
+ for code_block in blocks:
59
+ # 查找前置说明段落
60
+ prev_text = self._find_prev_text_block(
61
+ code_block, all_section_blocks, self.config.code_context_max
62
+ )
63
+ # 查找后置补充说明
64
+ next_text = self._find_next_text_block(
65
+ code_block, all_section_blocks, self.config.code_context_max
66
+ )
67
+
68
+ code_text = code_block.get("text", "")
69
+ code_tokens = estimate_tokens(code_text)
70
+ language = code_block.get("meta", {}).get("language", "")
71
+
72
+ if code_tokens <= self.config.code_token_max:
73
+ # 正常大小的代码块:直接绑定上下文成一个 Chunk
74
+ chunks.append(
75
+ self._make_code_chunk(
76
+ code_block=code_block,
77
+ prev_text_block=prev_text,
78
+ next_text_block=next_text,
79
+ section=section,
80
+ )
81
+ )
82
+ else:
83
+ # 超大代码块:按逻辑边界拆分后逐段处理
84
+ sub_chunks = self._split_large_code(
85
+ code_block=code_block,
86
+ prev_text_block=prev_text,
87
+ language=language,
88
+ section=section,
89
+ )
90
+ chunks.extend(sub_chunks)
91
+
92
+ return chunks
93
+
94
+ def _make_code_chunk(
95
+ self,
96
+ code_block: dict[str, Any],
97
+ prev_text_block: dict[str, Any] | None,
98
+ next_text_block: dict[str, Any] | None,
99
+ section: Section,
100
+ ) -> Chunk:
101
+ """
102
+ 将代码块与前后上下文拼接为一个 Chunk。
103
+
104
+ 拼接格式:
105
+ [前置说明](可选)
106
+ [空行]
107
+ [代码内容]
108
+ [空行]
109
+ [后置说明](可选)
110
+ """
111
+ parts: list[str] = []
112
+ source_blocks: list[dict[str, Any]] = []
113
+
114
+ if prev_text_block:
115
+ parts.append(prev_text_block.get("text", ""))
116
+ source_blocks.append(prev_text_block)
117
+
118
+ parts.append(code_block.get("text", ""))
119
+ source_blocks.append(code_block)
120
+
121
+ if next_text_block:
122
+ parts.append(next_text_block.get("text", ""))
123
+ source_blocks.append(next_text_block)
124
+
125
+ content = "\n\n".join(parts)
126
+ return Chunk(
127
+ content=content,
128
+ chunk_type=ChunkType.CODE,
129
+ title_path=list(section.title_path),
130
+ source_blocks=source_blocks,
131
+ token_count=estimate_tokens(content),
132
+ metadata={"language": code_block.get("meta", {}).get("language", "")},
133
+ )
134
+
135
+ def _split_large_code(
136
+ self,
137
+ code_block: dict[str, Any],
138
+ prev_text_block: dict[str, Any] | None,
139
+ language: str,
140
+ section: Section,
141
+ ) -> list[Chunk]:
142
+ """
143
+ 将超大代码块按逻辑边界(连续空行)拆分为多个 Chunk。
144
+
145
+ 拆分规则:
146
+ - 以连续 2 个或以上空行作为逻辑段的分隔点(通常是函数/类之间)
147
+ - 每段保留语言标识(代码围栏格式)
148
+ - 前置上下文只附加到第一段,避免上下文在每段重复,稀释语义
149
+ - 相邻段之间保留重叠:后一段包含前一段最后 overlap_token_size 个 token 的内容
150
+
151
+ Args:
152
+ code_block : 原始代码 Block
153
+ prev_text_block : 前置上下文(可为 None)
154
+ language : 代码语言标识
155
+ section : 当前章节
156
+
157
+ Returns:
158
+ 拆分后的 Chunk 列表
159
+ """
160
+ code_text = code_block.get("text", "")
161
+
162
+ # 按连续空行切分代码段
163
+ # re.split 以 2 个以上连续换行为分隔符
164
+ raw_segments = re.split(r"\n{2,}", code_text)
165
+ # 过滤掉空段
166
+ segments = [s.strip() for s in raw_segments if s.strip()]
167
+
168
+ if len(segments) <= 1:
169
+ # 无法找到逻辑边界,强制按字符数切割(兜底策略)
170
+ segments = self._force_split_by_size(code_text)
171
+
172
+ chunks: list[Chunk] = []
173
+ overlap_text = "" # 前一段的重叠尾部
174
+
175
+ for seg_idx, segment in enumerate(segments):
176
+ # 在重叠文本前加上语言标识围栏(便于向量模型识别代码类型)
177
+ if language:
178
+ seg_with_lang = f"```{language}\n{overlap_text}{segment}\n```"
179
+ else:
180
+ seg_with_lang = f"```\n{overlap_text}{segment}\n```"
181
+
182
+ parts: list[str] = []
183
+ source_blocks: list[dict[str, Any]] = [code_block]
184
+
185
+ # 前置上下文只附加到第一个子段
186
+ if seg_idx == 0 and prev_text_block:
187
+ parts.append(prev_text_block.get("text", ""))
188
+ source_blocks.insert(0, prev_text_block)
189
+
190
+ parts.append(seg_with_lang)
191
+ content = "\n\n".join(parts)
192
+
193
+ chunks.append(Chunk(
194
+ content=content,
195
+ chunk_type=ChunkType.CODE,
196
+ title_path=list(section.title_path),
197
+ source_blocks=source_blocks,
198
+ token_count=estimate_tokens(content),
199
+ metadata={
200
+ "language": language,
201
+ "segment_index": seg_idx,
202
+ "total_segments": len(segments),
203
+ },
204
+ ))
205
+
206
+ # 计算下一段的重叠内容:取当前段末尾 overlap_token_size 对应的字符
207
+ overlap_chars = int(self.config.overlap_token_size * 1.5)
208
+ overlap_text = segment[-overlap_chars:] if len(segment) > overlap_chars else segment
209
+ # 重叠文本加注释标记,让向量模型知道这是延续内容
210
+ overlap_text = f"# ... (续上段)\n{overlap_text}\n\n"
211
+
212
+ return chunks
213
+
214
+ def _force_split_by_size(self, code_text: str) -> list[str]:
215
+ """
216
+ 兜底策略:在无法找到逻辑边界时,按固定字符数强制切割代码。
217
+
218
+ 切割时尽量在换行处切割,避免在代码行中间截断。
219
+
220
+ Args:
221
+ code_text : 完整代码文本
222
+
223
+ Returns:
224
+ 切割后的代码片段列表
225
+ """
226
+ max_chars = int(self.config.code_token_max * 1.5)
227
+ segments = []
228
+ remaining = code_text
229
+
230
+ while len(remaining) > max_chars:
231
+ # 在 max_chars 附近找最近的换行处
232
+ cut_pos = remaining.rfind("\n", 0, max_chars)
233
+ if cut_pos <= 0:
234
+ cut_pos = max_chars # 实在找不到换行,强制截断
235
+ segments.append(remaining[:cut_pos])
236
+ remaining = remaining[cut_pos:].lstrip("\n")
237
+
238
+ if remaining:
239
+ segments.append(remaining)
240
+
241
+ return segments