99from typing import Dict , Any , List , Optional
1010from ..utils .llm_client import LLMClient
1111from ..utils .locale import get_language_instruction
12+ from ..utils .file_parser import split_text_into_chunks
1213
1314logger = logging .getLogger (__name__ )
1415
@@ -227,6 +228,10 @@ def generate(
227228
228229 # 传给 LLM 的文本最大长度(5万字)
229230 MAX_TEXT_LENGTH_FOR_LLM = 50000
231+ LONG_TEXT_CHUNK_SIZE = 8000
232+ LONG_TEXT_CHUNK_OVERLAP = 200
233+ MAX_LONG_TEXT_CHUNKS = 60
234+ MIN_LONG_TEXT_EXCERPT = 400
230235
231236 def _build_user_message (
232237 self ,
@@ -236,14 +241,7 @@ def _build_user_message(
236241 ) -> str :
237242 """构建用户消息"""
238243
239- # 合并文本
240- combined_text = "\n \n ---\n \n " .join (document_texts )
241- original_length = len (combined_text )
242-
243- # 如果文本超过5万字,截断(仅影响传给LLM的内容,不影响图谱构建)
244- if len (combined_text ) > self .MAX_TEXT_LENGTH_FOR_LLM :
245- combined_text = combined_text [:self .MAX_TEXT_LENGTH_FOR_LLM ]
246- combined_text += f"\n \n ...(原文共{ original_length } 字,已截取前{ self .MAX_TEXT_LENGTH_FOR_LLM } 字用于本体分析)..."
244+ combined_text = self ._build_document_context (document_texts )
247245
248246 message = f"""## 模拟需求
249247
@@ -273,6 +271,142 @@ def _build_user_message(
273271"""
274272
275273 return message
274+
275+ def _build_document_context (self , document_texts : List [str ]) -> str :
276+ """构建用于本体分析的文档上下文,长文本按全局分块抽样而不是只截取开头。"""
277+
278+ combined_text = "\n \n ---\n \n " .join (document_texts )
279+ original_length = len (combined_text )
280+
281+ if original_length <= self .MAX_TEXT_LENGTH_FOR_LLM :
282+ return combined_text
283+
284+ chunks = self ._collect_document_chunks (document_texts )
285+ if not chunks :
286+ return ""
287+
288+ selected_chunks = self ._select_representative_chunks (chunks )
289+ excerpt_budget = self ._calculate_excerpt_budget (len (selected_chunks ))
290+ context = self ._render_chunked_context (
291+ selected_chunks = selected_chunks ,
292+ original_length = original_length ,
293+ total_chunks = len (chunks ),
294+ excerpt_limit = excerpt_budget ,
295+ )
296+
297+ while len (context ) > self .MAX_TEXT_LENGTH_FOR_LLM and excerpt_budget > self .MIN_LONG_TEXT_EXCERPT :
298+ excerpt_budget = max (self .MIN_LONG_TEXT_EXCERPT , int (excerpt_budget * 0.85 ))
299+ context = self ._render_chunked_context (
300+ selected_chunks = selected_chunks ,
301+ original_length = original_length ,
302+ total_chunks = len (chunks ),
303+ excerpt_limit = excerpt_budget ,
304+ )
305+
306+ if len (context ) > self .MAX_TEXT_LENGTH_FOR_LLM :
307+ marker = "\n \n ...(分块上下文已压缩到本体分析长度限制内)..."
308+ context = context [:self .MAX_TEXT_LENGTH_FOR_LLM - len (marker )] + marker
309+
310+ return context
311+
312+ def _collect_document_chunks (self , document_texts : List [str ]) -> List [Dict [str , Any ]]:
313+ """按文档收集分块,保留文档和分块编号方便提示词定位。"""
314+
315+ all_chunks : List [Dict [str , Any ]] = []
316+ for doc_index , text in enumerate (document_texts , 1 ):
317+ doc_chunks = split_text_into_chunks (
318+ text ,
319+ chunk_size = self .LONG_TEXT_CHUNK_SIZE ,
320+ overlap = self .LONG_TEXT_CHUNK_OVERLAP ,
321+ )
322+ total_doc_chunks = len (doc_chunks )
323+ for chunk_index , chunk in enumerate (doc_chunks , 1 ):
324+ all_chunks .append ({
325+ "document_index" : doc_index ,
326+ "chunk_index" : chunk_index ,
327+ "total_document_chunks" : total_doc_chunks ,
328+ "text" : chunk ,
329+ })
330+
331+ return all_chunks
332+
333+ def _select_representative_chunks (self , chunks : List [Dict [str , Any ]]) -> List [Dict [str , Any ]]:
334+ """从全部分块中等距抽样,覆盖长文开头、中段和结尾。"""
335+
336+ if len (chunks ) <= self .MAX_LONG_TEXT_CHUNKS :
337+ return chunks
338+
339+ if self .MAX_LONG_TEXT_CHUNKS <= 1 :
340+ return [chunks [0 ]]
341+
342+ last_index = len (chunks ) - 1
343+ selected_indexes = {
344+ round (i * last_index / (self .MAX_LONG_TEXT_CHUNKS - 1 ))
345+ for i in range (self .MAX_LONG_TEXT_CHUNKS )
346+ }
347+ return [chunks [i ] for i in sorted (selected_indexes )]
348+
349+ def _calculate_excerpt_budget (self , selected_count : int ) -> int :
350+ """根据选中的分块数量为每块分配字符预算。"""
351+
352+ header_budget = 600
353+ chunk_header_budget = 120 * selected_count
354+ available = max (
355+ self .MIN_LONG_TEXT_EXCERPT * selected_count ,
356+ self .MAX_TEXT_LENGTH_FOR_LLM - header_budget - chunk_header_budget ,
357+ )
358+ return max (self .MIN_LONG_TEXT_EXCERPT , available // max (selected_count , 1 ))
359+
360+ def _render_chunked_context (
361+ self ,
362+ selected_chunks : List [Dict [str , Any ]],
363+ original_length : int ,
364+ total_chunks : int ,
365+ excerpt_limit : int ,
366+ ) -> str :
367+ """渲染长文本分块上下文。"""
368+
369+ lines = [
370+ (
371+ f"【长文本自动分块摘要】原文共{ original_length } 字,"
372+ f"已分为{ total_chunks } 个文本块用于全局覆盖分析。"
373+ ),
374+ (
375+ f"以下展示其中{ len (selected_chunks )} 个代表性文本块的摘录,"
376+ "覆盖开头、中段和结尾;请基于这些跨全文线索设计本体,不要只依赖第一段内容。"
377+ ),
378+ ]
379+
380+ for chunk in selected_chunks :
381+ excerpt = self ._excerpt_text (chunk ["text" ], excerpt_limit )
382+ lines .append (
383+ "\n " .join ([
384+ (
385+ f"--- 文档 { chunk ['document_index' ]} / "
386+ f"分块 { chunk ['chunk_index' ]} /{ chunk ['total_document_chunks' ]} ---"
387+ ),
388+ excerpt ,
389+ ])
390+ )
391+
392+ return "\n \n " .join (lines )
393+
394+ @staticmethod
395+ def _excerpt_text (text : str , char_limit : int ) -> str :
396+ """长分块保留首尾,避免每个分块内部再次变成只看开头。"""
397+
398+ text = text .strip ()
399+ if len (text ) <= char_limit :
400+ return text
401+
402+ marker = "\n ...(本分块中间内容省略)...\n "
403+ if char_limit <= len (marker ) + 20 :
404+ return text [:char_limit ]
405+
406+ remaining = char_limit - len (marker )
407+ head_len = remaining // 2
408+ tail_len = remaining - head_len
409+ return f"{ text [:head_len ].rstrip ()} { marker } { text [- tail_len :].lstrip ()} "
276410
277411 def _validate_and_process (self , result : Dict [str , Any ]) -> Dict [str , Any ]:
278412 """验证和后处理结果"""
@@ -503,4 +637,3 @@ def generate_python_code(self, ontology: Dict[str, Any]) -> str:
503637 code_lines .append ('}' )
504638
505639 return '\n ' .join (code_lines )
506-
0 commit comments