diff --git a/env.example b/env.example index a64626ef..3ce6d1d9 100644 --- a/env.example +++ b/env.example @@ -128,10 +128,12 @@ ENABLE_LLM_CACHE_FOR_EXTRACT=true ### Number of summary semgments or tokens to trigger LLM summary on entity/relation merge (at least 3 is recommented) # FORCE_LLM_SUMMARY_ON_MERGE=8 -### Number of tokens to trigger LLM summary on entity/relation merge -# SUMMARY_MAX_TOKENS = 500 +### Max description token size to trigger LLM summary +# SUMMARY_MAX_TOKENS = 1200 +### Recommended LLM summary output length in tokens +# SUMMARY_LENGTH_RECOMMENDED_=600 ### Maximum context size sent to LLM for description summary -# SUMMARY_CONTEXT_SIZE=10000 +# SUMMARY_CONTEXT_SIZE=12000 ############################### ### Concurrency Configuration diff --git a/lightrag/api/config.py b/lightrag/api/config.py index f4a281a7..a6badec4 100644 --- a/lightrag/api/config.py +++ b/lightrag/api/config.py @@ -30,6 +30,7 @@ from lightrag.constants import ( DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE, DEFAULT_MAX_ASYNC, DEFAULT_SUMMARY_MAX_TOKENS, + DEFAULT_SUMMARY_LENGTH_RECOMMENDED, DEFAULT_SUMMARY_CONTEXT_SIZE, DEFAULT_SUMMARY_LANGUAGE, DEFAULT_EMBEDDING_FUNC_MAX_ASYNC, @@ -133,6 +134,14 @@ def parse_args() -> argparse.Namespace: ), help=f"LLM Summary Context size (default: from env or {DEFAULT_SUMMARY_CONTEXT_SIZE})", ) + parser.add_argument( + "--summary-length-recommended", + type=int, + default=get_env_value( + "SUMMARY_LENGTH_RECOMMENDED", DEFAULT_SUMMARY_LENGTH_RECOMMENDED, int + ), + help=f"LLM Summary Context size (default: from env or {DEFAULT_SUMMARY_LENGTH_RECOMMENDED})", + ) # Logging configuration parser.add_argument( diff --git a/lightrag/constants.py b/lightrag/constants.py index 0e6d6dcd..2f493277 100644 --- a/lightrag/constants.py +++ b/lightrag/constants.py @@ -14,9 +14,14 @@ DEFAULT_MAX_GRAPH_NODES = 1000 DEFAULT_SUMMARY_LANGUAGE = "English" # Default language for summaries DEFAULT_MAX_GLEANING = 1 +# Number of description fragments to trigger LLM summary DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE = 8 -DEFAULT_SUMMARY_MAX_TOKENS = 500 # Max token size for entity/relation summary -DEFAULT_SUMMARY_CONTEXT_SIZE = 10000 # Default maximum token size +# Max description token size to trigger LLM summary +DEFAULT_SUMMARY_MAX_TOKENS = 1200 +# Recommended LLM summary output length in tokens +DEFAULT_SUMMARY_LENGTH_RECOMMENDED = 600 +# Maximum token size sent to LLM for summary +DEFAULT_SUMMARY_CONTEXT_SIZE = 12000 # Separator for graph fields GRAPH_FIELD_SEP = "" diff --git a/lightrag/lightrag.py b/lightrag/lightrag.py index f5529bad..f4dc9dd4 100644 --- a/lightrag/lightrag.py +++ b/lightrag/lightrag.py @@ -35,6 +35,7 @@ from lightrag.constants import ( DEFAULT_MIN_RERANK_SCORE, DEFAULT_SUMMARY_MAX_TOKENS, DEFAULT_SUMMARY_CONTEXT_SIZE, + DEFAULT_SUMMARY_LENGTH_RECOMMENDED, DEFAULT_MAX_ASYNC, DEFAULT_MAX_PARALLEL_INSERT, DEFAULT_MAX_GRAPH_NODES, @@ -293,6 +294,13 @@ class LightRAG: ) """Maximum number of tokens allowed per LLM response.""" + summary_length_recommended: int = field( + default=int( + os.getenv("SUMMARY_LENGTH_RECOMMENDED", DEFAULT_SUMMARY_LENGTH_RECOMMENDED) + ) + ) + """Recommended length of LLM summary output.""" + llm_model_max_async: int = field( default=int(os.getenv("MAX_ASYNC", DEFAULT_MAX_ASYNC)) ) @@ -435,9 +443,13 @@ class LightRAG: f"summary_context_size must be at least summary_max_tokens * force_llm_summary_on_merge, got {self.summary_context_size}" ) if self.summary_context_size > self.max_total_tokens: - logger.warning( + logger.error( f"summary_context_size must be less than max_total_tokens, got {self.summary_context_size}" ) + if self.summary_length_recommended > self.summary_max_tokens: + logger.warning( + f"summary_length_recommended should less than max_total_tokens, got {self.summary_length_recommended}" + ) # Fix global_config now global_config = asdict(self) diff --git a/lightrag/operate.py b/lightrag/operate.py index 6820401c..17dfa58c 100644 --- a/lightrag/operate.py +++ b/lightrag/operate.py @@ -114,6 +114,7 @@ def chunking_by_token_size( async def _handle_entity_relation_summary( + description_type: str, entity_or_relation_name: str, description_list: list[str], force_llm_summary_on_merge: int, @@ -172,6 +173,7 @@ async def _handle_entity_relation_summary( else: # Final summarization of remaining descriptions - LLM will be used final_summary = await _summarize_descriptions( + description_type, entity_or_relation_name, current_list, global_config, @@ -213,7 +215,11 @@ async def _handle_entity_relation_summary( else: # Multiple descriptions need LLM summarization summary = await _summarize_descriptions( - entity_or_relation_name, chunk, global_config, llm_response_cache + description_type, + entity_or_relation_name, + chunk, + global_config, + llm_response_cache, ) new_summaries.append(summary) llm_was_used = True # Mark that LLM was used in reduce phase @@ -223,7 +229,8 @@ async def _handle_entity_relation_summary( async def _summarize_descriptions( - entity_or_relation_name: str, + description_type: str, + description_name: str, description_list: list[str], global_config: dict, llm_response_cache: BaseKVStorage | None = None, @@ -247,18 +254,22 @@ async def _summarize_descriptions( "language", PROMPTS["DEFAULT_LANGUAGE"] ) + summary_length_recommended = global_config["summary_length_recommended"] + prompt_template = PROMPTS["summarize_entity_descriptions"] # Prepare context for the prompt context_base = dict( - entity_name=entity_or_relation_name, - description_list="\n".join(description_list), + description_type=description_type, + description_name=description_name, + description_list="\n\n".join(description_list), + summary_length=summary_length_recommended, language=language, ) use_prompt = prompt_template.format(**context_base) logger.debug( - f"Summarizing {len(description_list)} descriptions for: {entity_or_relation_name}" + f"Summarizing {len(description_list)} descriptions for: {description_name}" ) # Use LLM function with cache (higher priority for summary generation) @@ -563,7 +574,9 @@ async def _rebuild_knowledge_from_chunks( global_config=global_config, ) rebuilt_relationships_count += 1 - status_message = f"Rebuilt `{src} - {tgt}` from {len(chunk_ids)} chunks" + status_message = ( + f"Rebuilt `{src} - {tgt}` from {len(chunk_ids)} chunks" + ) logger.info(status_message) if pipeline_status is not None and pipeline_status_lock is not None: async with pipeline_status_lock: @@ -571,9 +584,7 @@ async def _rebuild_knowledge_from_chunks( pipeline_status["history_messages"].append(status_message) except Exception as e: failed_relationships_count += 1 - status_message = ( - f"Failed to rebuild `{src} - {tgt}`: {e}" - ) + status_message = f"Failed to rebuild `{src} - {tgt}`: {e}" logger.info(status_message) # Per requirement, change to info if pipeline_status is not None and pipeline_status_lock is not None: async with pipeline_status_lock: @@ -873,6 +884,7 @@ async def _rebuild_single_entity( if description_list: force_llm_summary_on_merge = global_config["force_llm_summary_on_merge"] final_description, _ = await _handle_entity_relation_summary( + "Entity", entity_name, description_list, force_llm_summary_on_merge, @@ -915,6 +927,7 @@ async def _rebuild_single_entity( force_llm_summary_on_merge = global_config["force_llm_summary_on_merge"] if description_list: final_description, _ = await _handle_entity_relation_summary( + "Entity", entity_name, description_list, force_llm_summary_on_merge, @@ -996,6 +1009,7 @@ async def _rebuild_single_relationship( force_llm_summary_on_merge = global_config["force_llm_summary_on_merge"] if description_list: final_description, _ = await _handle_entity_relation_summary( + "Relation", f"{src}-{tgt}", description_list, force_llm_summary_on_merge, @@ -1100,6 +1114,7 @@ async def _merge_nodes_then_upsert( if num_fragment > 0: # Get summary and LLM usage status description, llm_was_used = await _handle_entity_relation_summary( + "Entity", entity_name, description_list, force_llm_summary_on_merge, @@ -1218,6 +1233,7 @@ async def _merge_edges_then_upsert( if num_fragment > 0: # Get summary and LLM usage status description, llm_was_used = await _handle_entity_relation_summary( + "Relation", f"({src_id}, {tgt_id})", description_list, force_llm_summary_on_merge, @@ -1595,7 +1611,7 @@ async def merge_nodes_and_edges( ) # Don't raise exception to avoid affecting main flow - log_message = f"Completed merging: {len(processed_entities)} entities, {len(all_added_entities)} added entities, {len(processed_edges)} relations" + log_message = f"Completed merging: {len(processed_entities)} entities, {len(all_added_entities)} extra entities, {len(processed_edges)} relations" logger.info(log_message) async with pipeline_status_lock: pipeline_status["latest_message"] = log_message diff --git a/lightrag/prompt.py b/lightrag/prompt.py index 32666bb5..c8f8c3a3 100644 --- a/lightrag/prompt.py +++ b/lightrag/prompt.py @@ -133,20 +133,26 @@ Output: #############################""", ] -PROMPTS[ - "summarize_entity_descriptions" -] = """You are a helpful assistant responsible for generating a comprehensive summary of the data provided below. -Given one or two entities, and a list of descriptions, all related to the same entity or group of entities. -Please concatenate all of these into a single, comprehensive description. Make sure to include information collected from all the descriptions. -If the provided descriptions are contradictory, please resolve the contradictions and provide a single, coherent summary. -Make sure it is written in third person, and include the entity names so we the have full context. -Use {language} as output language. +PROMPTS["summarize_entity_descriptions"] = """---Role--- +You are a Knowledge Graph Specialist responsible for data curation and synthesis. + +---Task--- +Your task is to synthesize a list of descriptions of a given entity or relation into a single, comprehensive, and cohesive summary. + +---Instructions--- +1. **Comprehensiveness:** The summary must integrate key information from all provided descriptions. Do not omit important facts. +2. **Consistency:** If the descriptions contain contradictions, you must resolve them to produce a logically consistent summary. If a contradiction cannot be resolved, phrase the information neutrally. +3. **Context:** The summary must explicitly mention the name of the entity or relation for full context. +4. **Style:** The output must be written from an objective, third-person perspective. +5. **Conciseness:** Be concise and avoid redundancy. The summary's length must not exceed {summary_length} tokens. +6. **Language:** The entire output must be written in {language}. -####### ---Data--- -Entities: {entity_name} -Description List: {description_list} -####### +{description_type} Name: {description_name} +Description List: +{description_list} + +---Output--- Output: """