diff --git a/docs/benchmark-dspy-entity-extraction.md b/docs/benchmark-dspy-entity-extraction.md index e98f2f2..7c2aa51 100644 --- a/docs/benchmark-dspy-entity-extraction.md +++ b/docs/benchmark-dspy-entity-extraction.md @@ -1,4 +1,5 @@ -# Main Takeaways +# Chain Of Thought Prompting with DSPy-AI (v2.4.16) +## Main Takeaways - Time difference: 156.99 seconds - Execution time with DSPy-AI: 304.38 seconds - Execution time without DSPy-AI: 147.39 seconds @@ -6,7 +7,7 @@ - Relationships extracted: 21 (without DSPy-AI) vs 36 (with DSPy-AI) -# Results +## Results ```markdown > python examples/benchmarks/dspy_entity.py @@ -264,4 +265,12 @@ Relationships: "朱元璋早年为刘德放牛,这段经历对他的成长有重要影响。" - "朱元璋" -> "吴老太": "朱元璋曾希望托吴老太找一个媳妇,显示了他对家庭的渴望。" -``` \ No newline at end of file +``` + +# Self-Refine with DSPy-AI (v2.5.6) +## Main Takeaways +- Time difference: 66.24 seconds +- Execution time with DSPy-AI: 211.04 seconds +- Execution time without DSPy-AI: 144.80 seconds +- Entities extracted: 38 (without DSPy-AI) vs 16 (with DSPy-AI) +- Relationships extracted: 38 (without DSPy-AI) vs 16 (with DSPy-AI) diff --git a/examples/benchmarks/dspy_entity.py b/examples/benchmarks/dspy_entity.py index 955dd78..449fb4e 100644 --- a/examples/benchmarks/dspy_entity.py +++ b/examples/benchmarks/dspy_entity.py @@ -7,7 +7,8 @@ import time import shutil from nano_graphrag.entity_extraction.extract import extract_entities_dspy -from nano_graphrag._storage import NetworkXStorage, BaseKVStorage +from nano_graphrag.base import BaseKVStorage +from nano_graphrag._storage import NetworkXStorage from nano_graphrag._utils import compute_mdhash_id, compute_args_hash from nano_graphrag._op import extract_entities @@ -106,14 +107,12 @@ def print_extraction_results(graph_storage: NetworkXStorage): async def run_benchmark(text: str): print("\nRunning benchmark with DSPy-AI:") system_prompt = """ - You are a world-class AI system, capable of complex rationale and reflection. - Reason through the query, and then provide your final response. - If you detect that you made a mistake in your rationale at any point, correct yourself. - Think carefully. + You are an expert system specialized in entity and relationship extraction from complex texts. + Your task is to thoroughly analyze the given text and extract all relevant entities and their relationships with utmost precision and completeness. """ system_prompt_dspy = f"{system_prompt} Time: {time.time()}." - lm = dspy.OpenAI( - model="deepseek-chat", + lm = dspy.LM( + model="deepseek/deepseek-chat", model_type="chat", api_provider="openai", api_key=os.environ["DEEPSEEK_API_KEY"], @@ -127,7 +126,6 @@ async def run_benchmark(text: str): print(f"Execution time with DSPy-AI: {time_with_dspy:.2f} seconds") print_extraction_results(graph_storage_with_dspy) - import pdb; pdb.set_trace() print("Running benchmark without DSPy-AI:") system_prompt_no_dspy = f"{system_prompt} Time: {time.time()}." graph_storage_without_dspy, time_without_dspy = await benchmark_entity_extraction(text, system_prompt_no_dspy, use_dspy=False) @@ -148,7 +146,7 @@ async def run_benchmark(text: str): if __name__ == "__main__": - with open("./examples/data/test.txt", encoding="utf-8-sig") as f: + with open("./tests/zhuyuanzhang.txt", encoding="utf-8-sig") as f: text = f.read() asyncio.run(run_benchmark(text=text)) diff --git a/examples/using_dspy_entity_extraction.py b/examples/using_dspy_entity_extraction.py index 096eea1..e618e48 100644 --- a/examples/using_dspy_entity_extraction.py +++ b/examples/using_dspy_entity_extraction.py @@ -130,19 +130,12 @@ def query(): if __name__ == "__main__": - system_prompt = """ - You are a world-class AI system, capable of complex rationale and reflection. - Reason through the query, and then provide your final response. - If you detect that you made a mistake in your rationale at any point, correct yourself. - Think carefully. - """ - lm = dspy.OpenAI( - model="deepseek-chat", + lm = dspy.LM( + model="deepseek/deepseek-chat", model_type="chat", api_provider="openai", api_key=os.environ["DEEPSEEK_API_KEY"], base_url=os.environ["DEEPSEEK_BASE_URL"], - system_prompt=system_prompt, temperature=1.0, max_tokens=8192 ) diff --git a/nano_graphrag/entity_extraction/extract.py b/nano_graphrag/entity_extraction/extract.py index e72fa1c..45f1607 100644 --- a/nano_graphrag/entity_extraction/extract.py +++ b/nano_graphrag/entity_extraction/extract.py @@ -21,7 +21,7 @@ async def generate_dataset( save_dataset: bool = True, global_config: dict = {}, ) -> list[dspy.Example]: - entity_extractor = TypedEntityRelationshipExtractor() + entity_extractor = TypedEntityRelationshipExtractor(num_refine_turns=1, self_refine=True) if global_config.get("use_compiled_dspy_entity_relationship", False): entity_extractor.load(global_config["entity_relationship_module_path"]) @@ -84,7 +84,7 @@ async def extract_entities_dspy( entity_vdb: BaseVectorStorage, global_config: dict, ) -> Union[BaseGraphStorage, None]: - entity_extractor = TypedEntityRelationshipExtractor() + entity_extractor = TypedEntityRelationshipExtractor(num_refine_turns=1, self_refine=True) if global_config.get("use_compiled_dspy_entity_relationship", False): entity_extractor.load(global_config["entity_relationship_module_path"]) diff --git a/nano_graphrag/entity_extraction/module.py b/nano_graphrag/entity_extraction/module.py index 1ccce7f..3b12622 100644 --- a/nano_graphrag/entity_extraction/module.py +++ b/nano_graphrag/entity_extraction/module.py @@ -1,7 +1,7 @@ -from typing import Union import dspy from pydantic import BaseModel, Field from nano_graphrag._utils import clean_str +from nano_graphrag._utils import logger """ @@ -75,6 +75,14 @@ class Entity(BaseModel): description="Importance score of the entity. Should be between 0 and 1 with 1 being the most important.", ) + def to_dict(self): + return { + "entity_name": clean_str(self.entity_name.upper()), + "entity_type": clean_str(self.entity_type.upper()), + "description": clean_str(self.description), + "importance_score": float(self.importance_score), + } + class Relationship(BaseModel): src_id: str = Field(..., description="The name of the source entity.") @@ -96,6 +104,15 @@ class Relationship(BaseModel): description="The order of the relationship. 1 for direct relationships, 2 for second-order, 3 for third-order.", ) + def to_dict(self): + return { + "src_id": clean_str(self.src_id.upper()), + "tgt_id": clean_str(self.tgt_id.upper()), + "description": clean_str(self.description), + "weight": float(self.weight), + "order": int(self.order), + } + class CombinedExtraction(dspy.Signature): """ @@ -134,8 +151,85 @@ class CombinedExtraction(dspy.Signature): entity_types: list[str] = dspy.InputField( desc="List of entity types used for extraction." ) - entities_relationships: list[Union[Entity, Relationship]] = dspy.OutputField( - desc="List of entities and relationships extracted from the text." + entities: list[Entity] = dspy.OutputField( + desc="List of entities extracted from the text and the entity types." + ) + relationships: list[Relationship] = dspy.OutputField( + desc="List of relationships extracted from the text and the entity types." + ) + + +class CritiqueCombinedExtraction(dspy.Signature): + """ + Critique the current extraction of entities and relationships from a given text. + Focus on completeness, accuracy, and adherence to the provided entity types and extraction guidelines. + + Critique Guidelines: + 1. Evaluate if all relevant entities from the input text are captured and correctly typed. + 2. Check if entity descriptions are comprehensive and follow the provided guidelines. + 3. Assess the completeness of relationship extractions, including higher-order relationships. + 4. Verify that relationship descriptions are detailed and follow the provided guidelines. + 5. Identify any inconsistencies, errors, or missed opportunities in the current extraction. + 6. Suggest specific improvements or additions to enhance the quality of the extraction. + """ + + input_text: str = dspy.InputField( + desc="The original text from which entities and relationships were extracted." + ) + entity_types: list[str] = dspy.InputField( + desc="List of valid entity types for this extraction task." + ) + current_entities: list[Entity] = dspy.InputField( + desc="List of currently extracted entities to be critiqued." + ) + current_relationships: list[Relationship] = dspy.InputField( + desc="List of currently extracted relationships to be critiqued." + ) + entity_critique: str = dspy.OutputField( + desc="Detailed critique of the current entities, highlighting areas for improvement for completeness and accuracy.." + ) + relationship_critique: str = dspy.OutputField( + desc="Detailed critique of the current relationships, highlighting areas for improvement for completeness and accuracy.." + ) + + +class RefineCombinedExtraction(dspy.Signature): + """ + Refine the current extraction of entities and relationships based on the provided critique. + Improve completeness, accuracy, and adherence to the extraction guidelines. + + Refinement Guidelines: + 1. Address all points raised in the entity and relationship critiques. + 2. Add missing entities and relationships identified in the critique. + 3. Improve entity and relationship descriptions as suggested. + 4. Ensure all refinements still adhere to the original extraction guidelines. + 5. Maintain consistency between entities and relationships during refinement. + 6. Focus on enhancing the overall quality and comprehensiveness of the extraction. + """ + + input_text: str = dspy.InputField( + desc="The original text from which entities and relationships were extracted." + ) + entity_types: list[str] = dspy.InputField( + desc="List of valid entity types for this extraction task." + ) + current_entities: list[Entity] = dspy.InputField( + desc="List of currently extracted entities to be refined." + ) + current_relationships: list[Relationship] = dspy.InputField( + desc="List of currently extracted relationships to be refined." + ) + entity_critique: str = dspy.InputField( + desc="Detailed critique of the current entities to guide refinement." + ) + relationship_critique: str = dspy.InputField( + desc="Detailed critique of the current relationships to guide refinement." + ) + refined_entities: list[Entity] = dspy.OutputField( + desc="List of refined entities, addressing the entity critique and improving upon the current entities." + ) + refined_relationships: list[Relationship] = dspy.OutputField( + desc="List of refined relationships, addressing the relationship critique and improving upon the current relationships." ) @@ -159,7 +253,7 @@ def forward(self, **kwargs): except Exception as e: if isinstance(e, self.exception_types): - return dspy.Prediction(entities_relationships=[]) + return dspy.Prediction(entities=[], relationships=[]) raise e @@ -168,46 +262,63 @@ class TypedEntityRelationshipExtractor(dspy.Module): def __init__( self, lm: dspy.LM = None, - reasoning: dspy.OutputField = None, max_retries: int = 3, + entity_types: list[str] = ENTITY_TYPES, + self_refine: bool = False, + num_refine_turns: int = 1 ): super().__init__() self.lm = lm - self.entity_types = ENTITY_TYPES - self.extractor = dspy.TypedChainOfThought( - signature=CombinedExtraction, reasoning=reasoning, max_retries=max_retries - ) + self.entity_types = entity_types + self.self_refine = self_refine + self.num_refine_turns = num_refine_turns + + self.extractor = dspy.TypedChainOfThought(signature=CombinedExtraction, max_retries=max_retries) self.extractor = TypedEntityRelationshipExtractorException( self.extractor, exception_types=(ValueError,) ) + + if self.self_refine: + self.critique = dspy.TypedChainOfThought( + signature=CritiqueCombinedExtraction, + max_retries=max_retries + ) + self.refine = dspy.TypedChainOfThought( + signature=RefineCombinedExtraction, + max_retries=max_retries + ) def forward(self, input_text: str) -> dspy.Prediction: with dspy.context(lm=self.lm if self.lm is not None else dspy.settings.lm): extraction_result = self.extractor( input_text=input_text, entity_types=self.entity_types ) + + current_entities: list[Entity] = extraction_result.entities + current_relationships: list[Relationship] = extraction_result.relationships + + if self.self_refine: + for _ in range(self.num_refine_turns): + critique_result = self.critique( + input_text=input_text, + entity_types=self.entity_types, + current_entities=current_entities, + current_relationships=current_relationships + ) + refined_result = self.refine( + input_text=input_text, + entity_types=self.entity_types, + current_entities=current_entities, + current_relationships=current_relationships, + entity_critique=critique_result.entity_critique, + relationship_critique=critique_result.relationship_critique + ) + logger.debug(f"entities: {len(current_entities)} | refined_entities: {len(refined_result.refined_entities)}") + logger.debug(f"relationships: {len(current_relationships)} | refined_relationships: {len(refined_result.refined_relationships)}") + current_entities = refined_result.refined_entities + current_relationships = refined_result.refined_relationships - entities = [ - dict( - entity_name=clean_str(entity.entity_name.upper()), - entity_type=clean_str(entity.entity_type.upper()), - description=clean_str(entity.description), - importance_score=float(entity.importance_score), - ) - for entity in extraction_result.entities_relationships - if isinstance(entity, Entity) - ] - - relationships = [ - dict( - src_id=clean_str(relationship.src_id.upper()), - tgt_id=clean_str(relationship.tgt_id.upper()), - description=clean_str(relationship.description), - weight=float(relationship.weight), - order=int(relationship.order), - ) - for relationship in extraction_result.entities_relationships - if isinstance(relationship, Relationship) - ] + entities = [entity.to_dict() for entity in current_entities] + relationships = [relationship.to_dict() for relationship in current_relationships] return dspy.Prediction(entities=entities, relationships=relationships) diff --git a/tests/entity_extraction/test_module.py b/tests/entity_extraction/test_module.py index f52cf84..8c0d18f 100644 --- a/tests/entity_extraction/test_module.py +++ b/tests/entity_extraction/test_module.py @@ -1,13 +1,21 @@ +import pytest import dspy from unittest.mock import Mock, patch from nano_graphrag.entity_extraction.module import TypedEntityRelationshipExtractor, Relationship, Entity -def test_entity_relationship_extractor(): +@pytest.mark.parametrize("self_refine,num_refine_turns", [ + (False, 0), + (True, 2) +]) +def test_entity_relationship_extractor(self_refine, num_refine_turns): with patch('nano_graphrag.entity_extraction.module.dspy.TypedChainOfThought') as mock_chain_of_thought: input_text = "Apple announced a new iPhone model." mock_extractor = Mock() - mock_chain_of_thought.return_value = mock_extractor + mock_critique = Mock() + mock_refine = Mock() + + mock_chain_of_thought.side_effect = [mock_extractor, mock_critique, mock_refine] mock_entities = [ Entity(entity_name="APPLE", entity_type="ORGANIZATION", description="A technology company", importance_score=1), @@ -18,10 +26,20 @@ def test_entity_relationship_extractor(): ] mock_extractor.return_value = dspy.Prediction( - entities_relationships=mock_entities + mock_relationships + entities=mock_entities, relationships=mock_relationships ) - extractor = TypedEntityRelationshipExtractor() + if self_refine: + mock_critique.return_value = dspy.Prediction( + entity_critique="Good entities, but could be more detailed.", + relationship_critique="Relationships are accurate but limited." + ) + mock_refine.return_value = dspy.Prediction( + refined_entities=mock_entities, + refined_relationships=mock_relationships + ) + + extractor = TypedEntityRelationshipExtractor(self_refine=self_refine, num_refine_turns=num_refine_turns) result = extractor.forward(input_text=input_text) mock_extractor.assert_called_once_with( @@ -29,6 +47,10 @@ def test_entity_relationship_extractor(): entity_types=extractor.entity_types ) + if self_refine: + assert mock_critique.call_count == num_refine_turns + assert mock_refine.call_count == num_refine_turns + assert len(result.entities) == 2 assert len(result.relationships) == 1 diff --git a/tests/zhuyuanzhang.txt b/tests/zhuyuanzhang.txt new file mode 100644 index 0000000..0aab4a9 --- /dev/null +++ b/tests/zhuyuanzhang.txt @@ -0,0 +1,379 @@ +第一章 童年 + +我们从一份档案开始。 + +姓名:朱元璋别名(外号):朱重八、朱国瑞 + +性别:男 + +民族:汉 + +血型:? + +学历:无文凭,秀才举人进士统统的不是,后曾自学过 + +职业:皇帝 + +家庭出身:(至少三代)贫农 + +生卒:1328-1398 + +最喜欢的颜色:黄色(这个好像没得选) + +社会关系: + +父亲:朱五四,农民 + +母亲:陈氏,农民(不好意思,史书中好像没有她的名字) + +座右铭:你的就是我的,我还是我的 + +主要经历: + +1328年-1344年放牛 + +1344年-1347年做和尚,主要工作是出去讨饭(这个……) + +1347年-1352年做和尚主要工作是撞钟 + +1352年-1368年造反(这个猛) + +1368年-1398年主要工作是做皇帝 + +一切的事情都从1328年的那个夜晚开始,农民朱五四的妻子陈氏生下了一个男婴,大家都知道了,这个男婴就是后来的朱元璋。大凡皇帝出世,后来的史书上都会有一些类似的怪象记载。 + +比如刮风啊,下暴雨啊,冒香气啊,天上星星闪啊,到处放红光啊,反正就是要告诉你,这个人和别人不一样。朱元璋先生也不例外,他出生时,红光满地,夜间房屋中出现异光,以致于邻居以为失火了,跑来相救(明实录)。 + +然而当时农民朱五四的心情并不像今天我们在医院产房外看到的那些焦急中带着喜悦的父亲们,作为已经有了三个儿子、两个女儿的父亲而言,首先要考虑的是吃饭问题。 + +农民朱五四的工作由两部分构成,他有一个豆腐店,但主要还是要靠种地主家的土地讨生活,这就决定了作为这个劳动家庭的一员,要活下去只能不停的干活。 + +在小朱五四出生一个月后,父母为他取了一个名字(元时惯例):朱重八,这个名字也可以叫做朱八八,我们这里再介绍一下,朱重八家族的名字,都很有特点。 + +朱重八高祖名字:朱百六; + +朱重八曾祖名字:朱四九; + +朱重八祖父名字:朱初一; + +他的父亲我们介绍过了,叫朱五四。 + +取这样的名字不是因为朱家是搞数学的,而是因为在元朝,老百姓如果不能上学和当官就没有名字,只能以父母年龄相加或者出生的日期命名。(登记户口的人一定会眼花) + +朱重八的童年在一间冬凉夏暖、四面通风、采光良好的破茅草屋里度过,他的主要工作是为地主刘德家放牛。他曾经很想读书,可是朱五四是付不起学费的,他没有李密牛角挂书那样的情操,自然也没有杨素那样的大官来赏识他,于是,他很老实的帮刘德放了十二年的牛。 + +因为,他要吃饭。 + +在此时,朱重八的梦想是好好的活下去,到十六岁的时候,托村口的吴老太作媒,找一个手脚勤快、能干活的姑娘当媳妇,然后生下自己的儿女,儿女的名字可能是朱三二、或者朱四零,等到朱三二等人大了,就让他们去地主刘小德家放牛。 + +这就是十六岁时的朱重八对未来生活的幸福向往。 + +此时的中国,正在极其腐败的元王朝的统治下,那些来自蒙古的征服者似乎不认为在自己统治下的老百姓是人,他们甚至经常考虑把这些占地方的家伙都杀掉,然后把土地用来放牧(元史),从赋税到徭役,只要是人能想出来的科目,都能用来收钱,过节要收“过节钱”、干活有“常例钱”、打官司有“公事钱”,怕了吧,那我不出去还不行吗?不干事还不行吗?那也不行,平白无故也要钱,要收“撒花钱”。服了吧。 + +于是,在这个马上民族统治中国六十余年后,他们的国家机器已经到了无法承受的地步,此时的元帝国就好像是一匹不堪重负的骆驼,只等那最后一根稻草。 + +这根稻草很快就到了。 + +1344年是一个有特殊意义的年份,在这一年,上天终于准备抛弃元了,他给中国带来了两个灾难,同时也给元挖了一个墓坑,并写好了墓志铭:石人一只眼,挑动黄河天下反。 + +他想的很周到,还为元准备了一个填土的人:朱重八。 + +当然朱重八不会想到上天会交给他这样一个重要的任务。 + +这一年,他十七岁。 + +很快一场灾难就要降临到他的身上,但同时,一个伟大的事业也在等待着他,只有像传说中的凤凰一样,历经苦难,投入火中,经过千锤百炼,才能浴火重生,成为光芒万丈的神鸟。 + +朱重八,来吧,命运之神正在等待着你! + +第二章 灾难 + +元至正四年(公元1344年)到来了,这一年刚开始,元帝国的头头脑脑们就收到了两个消息,首先是黄河泛滥了,沿岸山东河南几十万人沦为难民。即使不把老百姓当人,但还要防着他们造反,所以修黄河河堤就成为了必须要做的事情。 + +可是令人意外的是,在元的政府中,竟然出现了两种不同的意见,一种认为一定要修,另一种认为不能修。在现在看来,这似乎是不可思议的事情,黄河泛滥居然不去修,难道要任黄河改道淹死那么多人?在中国历史上有着太多不可思议的事情,这个也不例外。 + +客观的讲,在这样一件事上,就维护元朝的统治而言,主要修的不一定是忠臣,反对修的也未必就是奸臣,其中奥妙何处?要到七年后才会见分晓。 + +极力主张修的是元朝的著名宰相脱脱,他可以说是元朝的最后一个名臣,实行了很多的改革政策,为政清廉,而且十分能干(宋史就是他主持修的),可是他没有想到的是,他的极力主张,已经给元朝埋下了一个大大的炸药包,拉好了引线,只等着那微弱的火光。 + +另一个是淮河沿岸遭遇严重瘟疫和旱灾,对于元政府来说,这个比较简单一点,反正饿死病死了就没麻烦了,当然表面功夫还是要做的,皇帝(元顺帝)要下诏赈灾,中书省的高级官员们要联系粮食和银两,当然了自己趁机拿一点也是可以理解的。赈灾物品拨到各路(元代地方行政单位),地方长官们再留下点,之后是州、县。一层一层下来,到老百姓手中就剩谷壳了。然后地方上的各级官员们上书向皇帝表示感谢,照例也要说些感谢天恩的话,并把历史上的尧舜禹汤与皇上比较一下,皇帝看到了报告,深感自己做了大好事,于是就在自己的心中给自己记上一笔。 + +皆大欢喜,皆大欢喜,大家都很满意。 + +但老百姓是不满意的,很多人都不满意。 + +朱重八肯定是那些极其不满意的人中的一个。 + +灾难到来后,四月初六朱重八的父亲饿死,初九大哥饿死,十二日,大哥长子饿死、二十二日,母亲饿死。 + +如果说这是日记的话,那应该是世界上最悲惨的日记之一。 + +朱重八的愿望并不过分,他只是想要一个家,想要自己的子女,想要给辛劳一生,从没欺负过别人,老实巴交的父母一个安详的晚年,起码有口饭吃。 + +他的家虽然不大,但家庭成员关系和睦,相互依靠,父母虽然贫穷,但每天下地干活回来仍然会带给重八惊喜,有时是一个小巧的竹蜻蜓,有时是地主家不吃的猪头肉,这就是朱重八的家,然而现在什么都没有了。 + +朱重八的姐姐已经出嫁,三哥去了倒插门。除了朱重八的二哥,这个家庭已经没有了其他成员。 + +十七岁的朱重八,眼睁睁的看着他的亲人一个一个死去,而他却无能为力。人世间最大的痛苦莫过于此! + +他唯一的宣泄方式是痛哭,可是哭完了,他还要面对一个重要的问题,要埋葬他的父母,可是没有棺材、没有寿衣、没有坟地,他只能去找地主刘德,求刘德看在父亲给他当了一辈子佃户的分上,找个地方埋了他爹。 + +刘德干净利落的拒绝了他,原因简单,你父母死了,关我何事,给我干活,我也给过他饭吃。 + +朱重八没有办法,只能和他的二哥用草席盖着亲人的尸体,然后拿门板抬着到处走,希望能够找到一个地方埋葬父母。可是天下虽大,到处都是土地,却没有一块是属于他们的。 + +幸好有好心人看到他们确实可怜,终于给了他们一块地方埋葬父母。“魂悠悠而觅父母无有,志落魄而泱佯”,这是后来能吃饱饭的朱元璋的情感回忆。 + +朱重八不明白,自己的父母在土地上耕作了一辈子,却在死后连入土为安都做不到。地主从来不种地,却衣食无忧。为什么?可他此时也无法思考这个问题,因为他也要吃饭,他要活下去。 + +在绝望的时候,朱重八不止一次的祈求上天,从道教的太上老君到佛教的如来佛祖,只要他能知道名字的,祈祷的唯一内容只是希望与父母在一起生活下去,有口饭吃。 + +但结果让他很失望,于是他那幼小的心灵开始变得冰冷,他知道没有人能救他,除了他自己。 + +复仇的火焰开始在他心底燃烧。 + +如此的痛苦,使他从脆弱到坚强。 + +为了有饭吃,他决定去当和尚。 + +【和尚的生涯】 + +朱重八选择的地方是附近的皇觉寺,在寺里,他从事着类似长工的工作,他突然发现那些和尚除了没有头发,对待他的态度比刘德好不了多少,这些和尚自己有田地,还能结婚(元代),如果钱多还可以去开当铺。 + +但他们也需要人给他们打杂,在那里的和尚不念经,不拜佛,甚至连佛祖金身也不擦,这些活自然而然的由刚进庙的新人朱重八来完成。 + +朱重八一直忍耐着,然而除了要做这些粗活外,他还要兼任清洁工,仓库保管员,添油工(长明灯)。即使这样,他还是经常挨骂,在那些和尚喝酒吃肉的时候,他还要擦洗香客踩踏的地板,每一个孤独的夜晚,他只能独坐在柴房中,看着窗外的天空,思念着只与自己相处了十余年的父母。 + +他已经很知足了,他能吃饱饭,这就够了,不是吗? + +然而命运似乎要锻炼他的意志,他入寺仅五十余天后,由于饥荒过于严重,所有的和尚都要出去化缘,所谓化缘就是讨饭,我们熟悉的唐僧同志每次的口头禅就是:悟空,你去化些斋来。用俗话来说就是,悟空,你去讨点饭来。我曾经考察过化缘这个问题,发现朱重八同志连化缘也被人欺负。由于和尚多,往往对化缘地有界定,哪些地方富点,就指派领导的亲戚去,那些地方穷,就安排朱重八同志去。 + +反正饿死也该,谁让你是朱重八。 + +朱重八被指派的地点是在淮西和河南。这里也是饥荒的主要地带,谁能化给他呢? + +然而,就从这里开始,命运之神向他微笑。 + +在游方的生活中,朱重八只能走路,没有顺风车可搭,是名副其实的驴行。他一边走,一边讨饭,穿城越村,挨家挨户,山栖露宿,每敲开一扇门,对他都是一种考验,因为面对他的往往只是白眼、冷嘲热讽,对朱重八来说,敲开那扇门可能意味着侮辱,但不敲那扇门就会饿死。 + +朱重八已经没有了父母,没有了家,他所有的只是那么一点可怜的自尊,然而讨饭的生活使他失去了最后的保护。要讨饭就不能有尊严。 + +生命的尊严和生存的压力,哪个更重要? + +是的,朱重八,只有失去一切,你才能明白自己的力量和伟大。 + +朱重八和别的乞丐不同,也正是因为不同,他才没有一直当乞丐(请注意这句话)。 + +在讨饭的时候,他仔细研究了淮西的地理、山脉、风土人情,他开阔了视野,丰富了见识,认识了很多豪杰(实际上也是讨饭者)。此时,他还有了自己的宗教信仰——明教,他相信当黑暗笼罩大地的时候,伟大的弥勒佛一定会降世的。其实就他的身世遭遇来说,他是不是真的相信弥勒倒是很难说的,我们有理由相信,他心中真正的弥勒是他自己。 + +但朱重八最重要的收获是:他已经从一个只能无助的看着父母死去的孩童,一个被人欺负后只能躲在柴堆里小声哭的杂役,变成了能坚强面对一切困难的战士。一个武装到心灵的战士。 + +长期的困难生活,最能磨练一个人的意志,有很多人在遇到困难后,只能怨天尤人,得过且过,而另外一些人虽然也不得不在困难面前低头,但他们的心从未屈服,他们不断的努力,相信一定能够取得最后的胜利。 + +朱重八毫无疑问是后一种。 + +如果说,在出来讨饭前,他还是一个不知所措的少年,在他经过三年漂泊的生活回到皇觉寺时,他已经是一个有自信战胜一切的人。 + +这是一个伟大的转变,很多人可能究其一辈子也无法完成。转变的关键在于心。 + +对于我们很多人来说,心是最柔弱的地方,它特别容易被伤害,爱情的背叛,亲情的失去,友情的丢失,都将是重重的一击。然而对于朱重八来说,还有什么不可承受的呢?他已经失去一切,还有什么比亲眼看着父母死去而无能为力,为了活下去和狗抢饭吃、被人唾骂,鄙视更让人痛苦!我们有理由相信,就在某一个痛苦思考的夜晚,朱重八把这个最脆弱的地方变成了最强大的力量的来源。 + +是的,即使你拥有人人羡慕的容貌,博览群书的才学,挥之不尽的财富,也不能证明你的强大,因为心的强大,才是真正的强大。 + +当朱重八准备离开自己讨饭的淮西,回到皇觉寺时,他仔细的回忆了这个他呆了三年的地方,思考了他在这里得到的和失去的,然后收拾自己的包裹踏上了回家的路。 + +也许我还会回来的,朱重八这样想。 + +第三章 踏上征途 + +至正十一年(公元1351年),上天给元朝的最后一根稻草终于压了下来,元朝的末日到了。 + +我们的谜底也揭开了,现在看来,脱脱坚决要求治黄河的愿望是好的,然而他不懂得那些反对的人的苦心,元朝那腐败到极点的官吏也是他所不了解的。现在他终于要尝到苦果了。 + +当元朝命令沿岸十七万劳工修河堤时,各级的官吏也异常兴奋,首先,皇帝拨给的修河工钱是可以克扣的,民工的口粮是可以克扣的,反正他们不吃不喝也事不关己。这就是一大笔收入,工程的费用也是可以克扣的,反正黄河泛滥也淹不死自己这些当官的。 + +这是管河务的,那么不管河务的怎么捞钱呢,其实也简单,既然这么大工程,必然有徭役指标,找几十个人,到各个乡村去,看到男人就带走,理由?修河堤,不想去?拿钱来。 + +没有钱?有什么值钱的都带走! + +可怜的脱脱,一个好的理论家,却不是一个实践家。 + +老把戏出场了,当民工们挖到山东时,他们从河道下挖出了一个一只眼睛石人,背部刻着石人一只眼,挑动黄河天下反。民工们突然发现,这正是他们在工地上传唱了几年的歌词。于是人心思动。 + +这真是老把戏,简直可以编成电脑程序,在起义之前总要搞点这种封建迷信,但也没办法,人家就吃这一套。 + +接着的事情似乎就是理所应当的了,几天后,在朱重八讨过饭的地方(颖州,今安徽阜阳),韩山童和刘福通起义了,他们的起义与以往起义并没有不同,照例要搞个宗教组织,这次是白莲教,当然既然敢起义,身份也应该有所不同,于是,可能是八辈子贫农的韩山童突然姓了赵,成了宋朝的皇室,刘福通也成了刘光世大将的后人。 + +他们的命运和以往第一个起义的农民领袖也类似,起义、被镇压、后来者居上,这似乎是陈胜吴广们的宿命。 + +尽管他们的起义形式毫无新意,但这并不妨碍他们的伟大和在历史上的地位,在史书上,将永远的纪录着:公元1351年,韩山童、刘福通第一个举起了反抗元朝封建统治的大旗。 + +自古以来,建立一个王朝很难,毁灭一个却相对容易得多,所谓墙倒众人推,破鼓万人捶,不是没有来由的。 + +在元代这个把人分为四个等级的朝代里,最高等级的蒙古人杀掉最低等级的南人,唯一的惩罚是赔偿一头驴,碰到个闲散民工之类的人,可能连驴都省了。蒙古贵族们的思维似乎很奇怪,他们即使在占据了中国后,好像仍然把自己当成客人,主人家的东西想抢就抢,想拿就拿,反正不关自己的事。在他们的思维中,这些南人只会忍受也只能忍受他们的折磨。 + +但他们错了,这些奴隶会起来反抗的,当愤怒和不满超过了限度,当连像狗一样生存下去都成为一种奢望的时候,反抗是唯一的道路。反抗是为了生存。 + +这把火终于烧起来了,而且是燎原之势。 + +在短短的一年时间里,看似强大的元帝国发生了几十起暴动,数百万人参加了起义军,即使那纵横天下无敌手的蒙古骑兵也不复当年之勇,无力拯救危局。元帝国就像一堵朽墙,只要再踢一脚,就会倒下来。 + +此时的朱重八却仍然在寺庙里撞着钟,从种种迹象看,他并没有参加起义军的企图。虽然他与元朝有着不共戴天的仇恨,但对于一个普通人朱重八来说,起义是要冒风险的,捉住后是要杀头的,这使得他不得不仔细的考虑。 + +在很多的书中,朱重八被塑造成一个天生英雄的形象,于是在这样的剧本里,天生英雄的朱重八一听说起义了,马上回寺庙里操起家伙就投奔了起义军,表现了他彻底的革命性等等。 + +我认为,这不是真实的朱重八。 + +作为一个正常人,在做出一个可能会掉脑袋的决定的选择上,是绝对不会如此轻率的,如果朱重八真的是这样莽撞的一个人,他就不是一个真正的英雄。 + +真正的朱重八是一个有畏惧心理的人,他遭受过极大的痛苦,对元有着刻骨的仇恨,但他也知道生的可贵,一旦选择了造反,就没有回头路。 + +知道可能面对的困难和痛苦,在死亡的恐惧中不断挣扎,而仍然能战胜自己,选择这条道路,才是真正的勇气。 + +我认为这样的朱重八才是真正的英雄,一个战胜自己,不畏惧死亡的英雄。 + +朱重八在庙里的生活是枯燥而有规律的,但这枯燥而规律的生活被起义的熊熊烈火打乱了。具有讽刺意义的是,具体打乱这一切的并不是起义军,而是那些元的官吏们。 + +在镇压起义军的战斗中,如果吃了败仗,是要被上司处罚的,但镇压起义的任务又是必须要完成的,于是元朝的官吏们毅然决然的决定,拿老百姓开刀,既然无法打败起义军,那就把那些可以欺负的老百姓抓去交差,把他们当起义军杀掉。 + +从这个角度来看,元的腐朽官吏为推翻元朝的统治实在是不遗余力,立了大功。 + +此时摆在朱重八面前的形势严重了,如果不去起义,很有可能被某一个官吏抓去当起义者杀掉,然后冠以张三或者李四的名字。但投奔起义军也有很大的风险,一旦被元军打败,也是性命难保。 + +就在此时,一封信彻底改变了他的命运。 + +他幼年时候的朋友汤和写了一封信给他,信的内容是自己做了起义军的千户,希望朱重八也来参加起义军,共图富贵,朱重八看过后,不动声色,将信烧掉了。他还没有去参加起义的心理准备。 + +然而晚上,他的师兄告诉他,有人已经知道了他看义军信件的事情,准备去告发他。 + +朱重八终于被逼上了绝路。 + +接下来的是痛苦的思考和抉择朱重八面前有三条路:一、守在寺庙里;二、逃跑;三、造反。 + +朱重八也拿不定主意,他找到了一个人,问他的意见,这个人叫周德兴,我们后面还要经常提到他。 + +周德兴似乎也没有什么好主意,他给朱重八的建议是算一卦(这是什么主意),看什么条路合适。 + +算卦的结果是“卜逃卜守则不吉,将就凶而不妨”,意思是逃跑,呆在这里都不吉利,去造反还可能没事。 + +朱重八明白自己已经没有退路了,自己不过想要老老实实的过日子,种两亩地,孝敬父母,却做不到,父母负担着沉重的田赋和徭役,没有一天不是勤勤恳恳的干活,还落得个家破人亡的下场。躲到寺庙里不过想混口饭吃,如今又被人告发,可能要掉脑袋。 + +忍无可忍。 + +那就反了吧!反他娘的! + +这是一个真实版本的逼上梁山,也是那封建时代贫苦农民的唯一选择。谁不珍惜自己的生命?谁愿意打仗?在活不下去时,那些农民被迫以自己的鲜血和生命去推动封建社会的发展,直至它的灭亡。 + +这是他们的宿命。 + +所以我认为中国历史上的农民起义确实是值得肯定的,他们也许不是那么厚道,他们也许有着自己的各种打算,但他们确实别无选择。 + +汤和就这样成了朱重八的第一个战友。他在今后的日子里将陪同朱重八一起走完这条艰苦的道路。 + +然而汤和也绝对不会想到,自己居然是唯一一个陪他走完这条路的人。 + +第四章 就从这里起步 + +至正十二年(公元1352年),濠州城。 + +城池的守卫者郭子兴正在他的元帅府里,苦苦思索着对策,濠州城已经被元军围了很久,这样下去是坚守不了多久了。 + +就在此时,手下的军士前来报告,抓住了一个奸细,要请令旗去杀人,如果是以往,郭子兴是不会过问的,让士兵直接拿了令旗去杀就是了,但今天,他开口问了一句:“你怎么知道那个人是奸细?”军士回答道:“这个人说是来投军的,现在元军围困,哪里还有人来投军,他一定是元军奸细。” + +郭子兴差点笑了出来,投军?元军快打进城来了,还有来投军的,这个借口可是真不高明,他不禁起了好奇心,想去看看这个奸细。 + +于是他骑马赶到了城门口,看见了一个相貌奇怪的人,用今天的话来说,这个人的相貌是地包天,下巴突出,更奇特的是,他的额头也是向前凸出的,具体形状大概类似独门兵器月牙铲,上下凸,中间凹(参见朱元璋同志画像)。 + +这个人当然就是我们的朱重八。 + +郭子兴走到朱重八的面前,让人松开绑,问他:“你是奸细么?来干什么?” + +朱重八平静的回答:“我不是奸细,我是来投军的。” + +郭子兴大笑:“什么时候了,还有人来投军,你不用狡辩,等会就把你拉出去杀头!” + +朱重八只是应了一声:“喔。” + +郭子兴看着朱重八的眼睛,希望能看到慌乱,这是他平时的乐趣之一。 + +但在这个人眼睛里,他看到的只有镇定。 + +郭子兴不敢小看这个人了,很明显,这是一个吓不倒的人。于是他认真的询问了朱重八的名字,来历,当朱重八说出是千户长汤和介绍他来时,郭子兴这才明白,这个人真的是来投军的。 + +朱重八给他的印象实在是太深了,于是他没有将朱重八编入汤和的部队,而是将他放在自己身边,当自己的亲兵(警卫员)。 + +在军队里,朱重八很快就表现出了他的才能,比起其他的农民兵士,他是一个很突出的人,不但作战勇敢,而且很有计谋,处事冷静,思虑深远(注意这个特点),而且很讲义气,有危险的时候第一个上,这一切都让他有了崇高的威信。加上他的同乡汤和帮忙,他在当士兵两个月后,被提拔为九人长,这是他的第一个官职。 + +作为郭子兴的亲兵长,朱重八是很称职的,他不像其他的士兵,从不贪图财物,每次得到战利品,就献给郭子兴,如果得到赏赐,就分给士兵,由于他很有天赋,自学过一些字,分析问题准确,郭子兴渐渐把他当成自己的智囊,朱重八在军中的地位也逐渐重要起来。 + +也就在此时,朱重八将他的名字改成了朱元璋,所谓璋,是一种尖锐的玉器,这个朱元璋实际上就是诛元璋,朱重八把他自己比成诛灭元朝的利器,而这一利器正是元朝的统治者自己铸造出来的。在今后的二十年里,他们都将畏惧这个名字。 + +【汤和】 + +在军队中,汤和算是个奇特的人,他在朱元璋刚参军时,已经是千户,但他却很尊敬朱元璋,在军营里,人们可以看到一个奇特的现象,官职高得多的汤和总是走在士兵朱元璋的后边,并且毫不在意他人的眼神,更奇特的是朱元璋似乎认为这是理所应当的事情,也没有推托过。 + +我们不得不佩服汤和的远见,他知道朱元璋远非池中物,用今天的话说,他很识实务。相信也正是这个优点,使得他能够在后来的腥风血雨中幸存下来。 + +在军队里,朱元璋娶了老婆,与后来的那些众多妃嫔相比,这个老婆可以算是朱元璋成功的关键因素之一。这个女孩是郭子兴的义女,她的父亲姓马,是郭子兴的朋友,后来死去,将这个女孩托付给郭子兴,女孩名字不详,军队里的人都叫她马姑娘。就这样,朱元璋成了元帅的女婿,而郭子兴则多了一个帮手。 + +我们可以想象到朱元璋喜悦的心情,他终于有了一个自己的家,不再是那个没人管、没人问的朱重八,他饿了,有人做饭给他吃,冷了,有人送衣服给他,有家的感觉真好。这种感情一直陪伴了他很多年。 + +此时,朱元璋已经升任了军队中的总管,这个职位大致相当于起义军的办公室主任,他干得不错,对于某些喜欢贪公家便宜,胡乱报销的人,朱元璋是讲原则的,由于他严于律己,大家也没有什么话说,如果就这么干下去,他可能会成为一个优秀的财务管理人员。可是上天偏偏不让他舒服的过下去,不久的将来,他将面对更大的麻烦。 + +主要问题是,郭子兴的成分问题,他并不是农民,而是地主(想不通他怎么会起义),当时在濠州的统帅除了郭子兴外,还有四个人,以孙德崖为首,而这四个人都是农民,他们和郭子兴之间存在着深刻的矛盾。 + +不久,矛盾爆发了,一天郭子兴在濠州城里逛街,突然被一群来路不明的人绑票,这些人似乎对索取酬金之类也没有什么兴趣,把郭子兴死打一顿,然后关了禁闭。朱元璋得到消息,大吃一惊,立刻赶去孙德崖家里要人,孙德崖开始还装傻,表情惊讶,要出去找郭子兴,并且说了一些与绑架者不共戴天之类的话,充分表现出了一个业余演员的演技。 + +朱元璋只把参与打人的军士带到孙德崖面前,并且告诉孙,你的那些贪污公款、胡乱报销的烂账都在我这里,自己看着办。 + +于是,朱元璋从孙家的地窖中将已经打得半死的郭子兴救了出来,这件事情让朱元璋意识到,跟着这些人不会有前途。 + +而郭子兴也越来越讨厌朱元璋,原因很简单,朱元璋比他强,对于郭子兴这样一个性情暴躁、不能容人的统帅来说,他是不能容忍一个可能取代他地位的人在身边的。终于有一天,他把朱元璋关了起来,落井下石一向是某些人的优良传统,郭子兴的儿子就是某些人中的一个。他吩咐守兵不能给朱元璋送饭,想要把朱元璋饿死,善良的马姑娘为了救朱元璋,便把刚烫好的烙饼揣在怀中,到牢中探望朱元璋时送给他吃,每次胸口都会烫伤,但每次都送。 + +有妻如此,夫复何求。 + +郭子兴毕竟还是不想杀朱元璋,于是将他放了出来,朱元璋经历此事后,终于下了决心,和这些鼠目寸光的人决裂。他向郭子兴申请带兵出征,郭子兴高兴的答应了。 + +这就是朱元璋霸业的开始,一旦开始,就不会停止。 + +就从这里起步吧! + +朱元璋奉命带兵攻击郭子兴的老家,定远,从这一点可以看出他的岳父实在存心不良,当时的定远有重兵看守,估计郭子兴让他去就是不想再看到活着的朱元璋,但朱元璋就是朱元璋,他找到了元军的一个缝隙,攻克了定远,然后在元军回援前撤出,此后,连续攻击怀远、安奉、含山、虹县,四战四胜,锐不可当! + +在召集(也可能是抢)了壮丁后,朱元璋来到了钟离(今安徽凤阳东面),这是他的家乡,在这里他遇到了二十四个来朱元璋队伍里找工作的人。 + +朱元璋经理招收的二十四个人素质是相当高的,这其中有为他算过命的周德兴,还有堪称天下第一名将的徐达。 + +这些人还有亲戚,一传十,十传百,什么叔叔、舅舅、子侄、外甥都来了,很快,他的部队(直属)就有了七百人。 + +当朱元璋再次回到濠州的时候,他已经完全明白了自己的前途所在,所以他向郭子兴辞职,郭子兴非常高兴,这个讨厌的人终于可以走得远远的了。 + +朱元璋在出发前,又做了一件出人意料的事,他从自己的七百人中重新挑选了二十四个人,然后将其余的人都给了郭子兴,郭子兴多少有些意外,但仍然高兴的接受了。 + +朱元璋的这个行动似乎可以定义为一次挑选公务员的工作,比例是三十比一,没有笔试,考官就是朱元璋和他的眼光。 + +他挑的确实很准,看看这些人的名字:徐达、汤和、周德兴,这二十四个人后来都成为了明王朝的高级干部。 + +唐时的黄巢在考试落榜后,站在长安城门前,惆怅之余,豪气丛生,作诗一首,大大的有名——《咏菊》: + +〖待得秋来九月八,我花开时百花杀。 + +冲天香阵透长安,满城尽带黄金甲。〗 + +数年后,他带领着十余万大军,打进长安。 + +此时的朱元璋,站在濠州的城门前,看着自己身后的二十四个人,他知道,迈出这一步,他就将孤军奋战,或者兵败身死,或者开创霸业。 + +他仰望天空,还是那样阴暗,这个时候作出这个选择,似乎并不吉利,他又想起了那次无奈的占卜。 + +父母去世的时候,在庙里干苦力的时候,夜里望天痛哭的时候,也是这样的天空。 + +什么都没有变,变的只是我而已。 + +〖百花发时我不发,我若发时都吓杀。 + +要与西风战一场,遍身穿就黄金甲。〗 + +什么都不能阻挡我,就从这里开始吧! + +出发!