TO DO:搭建企业知识库RAG系统
系统特点:
这是竞赛代码,虽然功能完善但存在临时解决方案,比如IBM Watson集成部分是竞赛特定的,无法正常工作。
企业RAG比赛:https://github.com/trustbit/enterprise-rag-challenge/tree/main
基于公司年度报告构建一个问答系统,简单来说,比赛当天的流程如下:

获胜方案在native RAG的基础上,加入了两个路由器(router)和 LLM 重排序模块(LLM reranking)


Thinking:基础 RAG系统的开发流程是什么?
Thinking:解析模块的挑战?
需要将 PDF 文档转换为纯文本。PDF 解析是一项远非易事的工作, 其中充满了无数细微的难题:

Thinking:如何选择解析器?
作者尝试了20多种解析器:
最终作者选择了 Docling 作为PDF解析器, github
没有任何解析器可以处理所有细微之处,并在不丢失部分重要信息的情 况下,将 PDF 内容完全还原为文本。
尽管 Docling 的结果非常优秀,但它缺乏一些基本能力。作者研究了docling源代码,并重写了几个方法 以满足需求:

使用Docling,可以将报告解析为 JSON 格式
Thinking:是否需要进行表格序列化?
在大型表格中,度量名称(横向表头)通常离纵向表头太远,削弱了语义连贯性。

Thinking:大型表格理解的挑战?
理论上,大型表格会降低在向量搜索中 chunk 的相关性(同时,也存在表格不能完全装进一个 chunk 的情况)。 此外,LLM 在处理大型表格时也很难将度量名称与表头对应起来,可能会返回错误的值。
表格序列化(Serialization of tables)成为了解决方案。
关于这个主题的研究较少 => 需要独立探索,我们可以搜索 Row-wise Serialization(行式序列化)、Attribute- Value Pairing(属性-值对匹配),参考阅读 https://arxiv.org/pdf/2402.17944
结论:作者在测试不同配置时,惊讶地发现我们对它寄予厚望的表格序列化功能不仅没有改进系统, 反而略微降低了其有效性 => 所以实际使用上,并没有采用 表格序列化的方法
Thinking:让LLM对表格数据进行理解,是采用Markdown格式,还是html格式?
我们以Markdown 格式向 LLM 输入表格,但后来改用 HTML 格式。
语言模型对 HTML 的理解程度要高得多,而且 HTML 可以描述包含合并单元格、子标题和其他复杂结构的表格。 序列化代码见:tables_serialization.py
报告已从 PDF 转换为 => Markdown 文本 => 接下来,需要用它们来创建数据库。
根据比赛规则,必须指明包含相关信息的页码。企业系统也采用同样的方法:引用允许验证模型的答案是否 是虚假信息(hallucinated)。这不仅使系统对用户更透明,也简化了开发过程中的调试。
Thinking:如何对文档进行切分?使用一页作为一个chunk,还是300个token作为一个chunk?
最简单的选择是将文档的整页作为一个块,因为页面很少超过几千个 token。
但让我们再次思考查询和文档文本块之间的语义连贯性。通常情况下,能回答问题的信息片段不超过十个句 子。因此,从逻辑上讲,一个包含目标语句的小段落将比同样语句稀释在一整页相关性较弱的文本中获得更 高的相似度得分。
=> 将每页的文本分割成 300 个 token(约 15 个句子)的块。
每个块都存储其 ID 及其在元数据(metadata)中的父页面编号。
Thinking:如果我们有100家公司的文档,是创建一个Faiss数据库,还是100个Faiss数据库?
100 个数据库,其中 1 个数据库 = 1 个文档。
为什么要 将所有公司的信息混合在一起,之后再试图将一个公司的收入与另一个公司分开呢?
=> 基于公司为单位创建的 Faiss数据库,结构更清晰
=> 想看哪个公司的情况,就直接从该faiss库中进行检索
Faiss数据库使用IndexFlatIP方法创建。
Flat 索引的优点是所有向量都“原样”存储,没有压缩或量化。搜索使用暴力搜索,精度更高。 缺点是这种搜索计算和内存消耗更高。
为了将块和查询嵌入到向量表示中,作者使用了text-embedding-3-large(这个需要根据需求,进行替换)
RAG 系统中的R(检索)是一个通用的搜索系统,它接收查询作为输入,返回回答所需的 相关文本。 在基础实现中,它只是对向量数据库发起查询,提取 Top N 个结果。
Thinking:Retrieval是 RAG 系统中的关键部分,是否需要采用混合搜索?
如果 LLM 在查询上下文中没有接收到必要信息,它就无法提供正确答案(无论我们如何精心设置提示词) 垃圾进 → 垃圾出(Garbage in → Garbage out)。
常用的策略是:采用混合搜索
=> vDB + BM25混合搜索(Hybrid search),结合了基于向量语义和传统关键词的搜索( BM25 算法)。 理论上,它通过不仅考虑文本语义,还考虑精确关键词匹配来提高检索准确性。
=> 将两种方法的搜索结果合并,再进行重排序。
Jina Reranker 是一款专为提升信息检索性能设计的神经网络重排序模型,尤其适用于RAG:
Jina Reranker和bge-m3都是常用的Reranking模型,但在比赛中作者使用功了LLM重排序
Thinking:如何实现LLM重排序?
system_prompt_rerank_single_block = """
你是一个RAG检索重排专家。
你将收到一个查询和一个检索到的文本块,请根据其与查询的相关性进行评分。
评分说明:
1. 推理:分析文本块与查询的关系,简要说明理由。
2. 相关性分数(0-1,步长0.1):
0 = 完全无关
0.1 = 极弱相关
0.2 = 很弱相关
0.3 = 略有相关
0.4 = 部分相关
0.5 = 一般相关
0.6 = 较为相关
0.7 = 相关
0.8 = 很相关
0.9 = 高度相关
1 = 完全匹配
3. 只基于内容客观评价,不做假设。
"""
LLM 查询结果进行格式化输出,包含两个字段: reasoning(让模型解释其判断)和 relevance_score,可以直接从 JSON 中提取。
修正后的相关性得分,使用加权平均计算:
vector_weight = 0.3,
llm_weight = 0.7
理论上,我们可以直接跳过向量搜索,将每一页 都直接传递给 LLM。然而,使用embedding进行更 便宜、更快速的过滤仍然是必要的。
对于一份 1000 页的文档,仅仅回答一个问题就可 能花费约 25 美分——太昂贵了。
Thinking:我们之前将文本分割成小块chunk,这里是否还需要引入其父节点?
虽然回答问题的核心信息通常集中在某个小块中(这也是分块能提升检索效果的原因),但同一页面的其他 部分可能仍包含次要却重要的细节。
因此,在实际应用中,我们会先检索出 Top N 个最相关的文本块
=> 这些块仅作为“指针”来定位对应的完整页面。
=> 随后,我们会将整个页面的内容纳入上下文(context)中进行分析。
这就是为什么我们在每个块的元数据中记录其所属的页面编号——以便快速回溯原始内容。

最终检索器的步骤:
目前为止,向量数据库已建立,RAG 系统中的“R”(检索)已完成,我们现在进入“A”(增强)部分。
这部分相当直接,主要是 f-string 字符串拼接操作。
Thinking:如何更有效的管理提示词存储的方式?
在项目中,尝试了不同方法后,最终确定:将提示词存储在一个专门的prompts.py文件中,并将提示词分割成 逻辑块:
告诉大模型“你是谁、你要做什么、你要遵循哪些规则”的主提示词。
class AnswerWithRAGContextSharedPrompt:
instruction = """
你是一个RAG(检索增强生成)问答系统。
你的任务是仅基于公司年报中RAG检索到的相关页面内容,回答给定问题。
在给出最终答案前,请详细分步思考,尤其关注问题措辞。
- 注意:答案可能与问题表述不同。
- 问题可能是模板生成的,有时对该公司不适用。
"""
user_prompt = """
以下是上下文:
\"\"\"
{context}
\"\"\"
---
以下是问题:
"{question}"
"""
用 Pydantic 的 BaseModel 定义 LLM 输出的 JSON 格式,强制 LLM 输出结构化内容,便于后续解析和校验。
class ComparativeAnswerPrompt:
instruction = """
你是一个问答系统。
你的任务是基于各公司独立答案,给出原始比较问题的最终结论。
只能基于已给出的答案,不可引入外部知识。
请分步详细推理。
比较规则:
- 问题要求选出公司时,答案必须与原问题公司名完全一致
- 若某公司数据币种不符,需排除
- 若全部公司被排除,返回'N/A'
- 若仅剩一家,直接返回该公司名
"""
user_prompt = """
以下是单个公司的回答:
\"\"\"
{context}
\"\"\"
---
以下是原始比较问题:
"{question}"
"""
class AnswerSchema(BaseModel):
"""比较类问题最终答案的结构定义。"""
step_by_step_analysis: str = Field(description="详细分步推理过程,至少5步,150字以上。")
reasoning_summary: str = Field(description="简要总结推理过程,约50字。")
relevant_pages: List[int] = Field(description="保持为空列表。")
final_answer: Union[str, Literal["N/A"]] = Field(description="公司名称需与问题中完全一致。答案只能是单个公司名或'N/A'。")
pydantic_schema = re.sub(r"^ {4}", "", inspect.getsource(AnswerSchema), flags=re.MULTILINE)
example = r"""
示例:
问题:
"下列公司中,哪家2022年总资产最低:"A公司", "B公司", "C公司"?若无数据则排除。"
答案:```
{
"step_by_step_analysis": "1. 问题要求比较多家公司2022年总资产。\n2. 各公司独立答案:A公司6,601,086,000元,B公司1,249,642,000元,C公司217,435,000元。\n3. 直接比较得C公司最低。\n4. 若有公司币种不符则排除。\n5. 因此答案为C公司。",
"reasoning_summary": "独立答案显示C公司总资产最低,直接得出结论。",
"relevant_pages": [],
"final_answer": "C公司"
}```
"""
system_prompt = build_system_prompt(instruction, example)
system_prompt_with_schema = build_system_prompt(instruction, example, pydantic_schema)
给 LLM 一个或几个“问题-答案”示例,帮助它学会输出你想要的格式和风格。
example = r"""
示例:
问题:
"下列公司中,哪家2022年总资产最低:"A公司", "B公司", "C公司"?若无数据则排除。"
答案:```
{
"step_by_step_analysis": "1. 问题要求比较多家公司2022年总资产。\n2. 各公司独立答案:A公司6,601,086,000元,B公司1,249,642,000元,C公司217,435,000元。\n3. 直接比较得C公司最低。\n4. 若有公司币种不符则排除。\n5. 因此答案为C公司。",
"reasoning_summary": "独立答案显示C公司总资产最低,直接得出结论。",
"relevant_pages": [],
"final_answer": "C公司"
}```
"""
让 LLM 能够“看到”相关内容和问题,基于这些内容作答。
user_prompt = """
以下是上下文:
\"\"\"
{context}
\"\"\"
---
以下是问题:
"{question}"
"""
prompts.py 文件就是把所有和 LLM 交互相关的提示词、 格式、示例、模板都集中管理,并且分成了四个逻辑块:
RAG 中的 “G”(生成)是最耗费精力的。要在这一阶段实现高质量,需要巧妙地使用几种技术。
比赛场景:
比赛场景与实际应用中的处理理念是一样的,其核心逻辑: 1. 提取公司名 → 2. 匹配对应数据库 → 3. 仅搜索该库。
这样可将搜索范围缩小 100 倍,提升效率。

比赛要求:答案必须简洁且严格匹配指定数据类型(如 int/float/bool/str/list[str]),类似数据库存储格式。
挑战:
解决方案:
将复杂查询拆解为多个简单步骤,减少单次请求的规则数量。例如:
分步操作能显著提升准确性。
核心思路:简化任务 → 降低 LLM 认知负荷 → 确保格式零错误。

在比赛中,由于每个问题的预期响应类型(如 int/float/bool/str/list[str])都是明确给定的,我们可以采用更直接的解决方案:
为每种数据类型预先设计了专门的提示词模板,然后通 过简单的if-else逻辑来自动选择对应的提示词版本。
这种方法既保证了回答格式的精确性,又实现了高效的 自动化处理。

在比赛中,处理涉及多家公司比较的复杂问题时,采用分步拆解。
比如:苹果和微软谁的营收更高?
1. 问题拆解阶段
2. 并行查询阶段
3. 综合判断阶段
这种模块化处理模式具有高度可扩展性,能够灵活应对各类复杂比较查询
思维链(CoT)通过让模型在给出最终答案之前“出声思考”,显著提高了答案质量。
早期提示词工程中,一步步思考(Think step by step) 这类通用指令能提升答案质量,但对复杂任 务仍不足。
尤其较弱模型(如GPT-4o-mini、Llama 8b)常出现“虚假推理”
———— 先给答案再倒推理由,甚至捏造事实。
为了确保CoT 的作用,我们必须清晰地引导模型如何进行推理。解释推理步骤、目标、并提供示例。
带有模糊上下文的推理示例:
问题:Ritter Pharmaceuticals Inc. 的研发设备(按成本计)是多少?
模型的推理步骤:
问题询问 Ritter Pharmaceuticals Inc. 的“研发设备,按成本计”。这表明需要从资产负债表中提取一个特定数值,代表 专门用于研发的设备的原始购置成本,不包含任何累计折旧。
上下文(第 35 页)显示“不动产和设备,净额”为 12,500 美元。这是一个净值(已扣除折旧),而且是一个更广泛的类别,包含所有不动产和设备,而不仅仅是研发设备。
上下文(第 37 页)还提到“机械和设备”的“累计折旧”为 110,000 美元。这代表了总的折旧,而不是原始成本,更重要的是,它没有说明这些设备专门用于研发。
这两个指标都不完全符合请求的指标。“不动产和设备,净额”范围太广,且是折旧后的价值。“累计折旧”仅显示折 旧,而非成本,并且缺乏研发的针对性。
由于上下文没有提供仅用于研发设备的原始成本,且我们无法进行假设、计算,因此答案是“N/A”。
结构化输出(Structured Output,SO) 是一种强制模型 返回标准化格式(如JSON/Pydantic schema)的方法。
通过API传递schema,确保输出始终符合预定结构。
比如,这里是用于 LLM 重排序的 Pydantic schema
class RetrievalRankingSingleBlock(BaseModel):
"""对检索到的单个文本块与查询的相关性进行评分。"""
reasoning: str = Field(description="分析该文本块,指出其关键信息及与查询的关系")
relevance_score: float = Field(description="相关性分数,取值范围0到1,0表示完全无关,1表示完全相关")
思维链 + 结构化输出 可以理想地结合使用:
在生成阶段,模型有一个专门用于推理的字段,还有一个单独的字段用于最终答案 => 这样我们可以直接提取答案, 而无需从冗长的推理步骤中进行解析。
在用于回答比赛问题的主要schema 中,设置了四个字段:
Thinking:什么是指令细化?
当AI回答用户问题时,需要判断"答案的灵活范围"——比如问"CEO是谁",实际要包括哪些类似职位?(如总裁、董事 总经理等)
这种问题,在现实中经常遇到,而且现实数据不完美,即不同公司用不同头衔称呼老大(美国叫CEO,英国可能叫MD) 用户真正想知道的可能比字面意思更广 => 所以需要对 instruction 进行细化
Thinking:指令细化的挑战是什么?
比如:问"股息政策有变吗?",若报告未提及,该回答"无变更"还是"无信息"?
解决方案:
真实案例对比:
灵活回答(适合开放问答):
"Ethan Caldwell是董事总经理(最接近CEO的角色),但目前因调查暂停职务..."
严格回答(适合简答比赛):
需预先定义是否允许"MD≈CEO",否则可能输出错误简答
指令细化,这一部分的工作量与整个数据准备阶段不相上下,因为需要进行无止境的迭代调试、校对 答案以及手动分析模型的推理过程。
在比赛前一周,作者团队利用公开的问题生成器快速创建了100个问题的验证集,并通过手动回答这些问题获得 了两个重要收益:
最终,这些洞察被系统化地整合到提示词设计中,形成更严谨的指令框架。
在Prompt.py中有多种 prompt,代表不同的任务说明、结构化输出要求和示例,比如:
这些 prompt 直接决定了 LLM 的回答风格和结构。
在主流程(如 api_requests.py)中,处理每个问题时会读取 kind 字段,然后根据 kind 选择不同的 prompt 模板,比如:
kind: "boolean" → 用 AnswerWithRAGContextBooleanPrompt
kind: "number" → 用 AnswerWithRAGContextNumberPrompt
kind: "name" → 用 AnswerWithRAGContextNamePrompt
kind: "names" → 用 AnswerWithRAGContextNamesPrompt
对应 kind = “boolean” 的prompt:
class AnswerWithRAGContextBooleanPrompt:
instruction = AnswerWithRAGContextSharedPrompt.instruction
user_prompt = AnswerWithRAGContextSharedPrompt.user_prompt
class AnswerSchema(BaseModel):
step_by_step_analysis: str = Field(description="""
详细分步推理过程,至少5步,150字以上。特别注意问题措辞,避免被迷惑。有时上下文中看似有答案,但可能并非所问内容,仅为相似项。
""")
reasoning_summary: str = Field(description="简要总结分步推理过程,约50字。")
relevant_pages: List[int] = Field(description="""
仅包含直接用于回答问题的信息页面编号。只包括:
- 直接包含答案或明确陈述的页面
- 强有力支持答案的关键信息页面
不要包含仅与答案弱相关或间接相关的页面。
列表中至少应有一个页面。
""")
final_answer: Union[bool] = Field(description="""
一个从上下文中精确提取的布尔值(True或False),直接回答问题。
如果问题问某事是否发生,且上下文有相关信息但未发生,则返回False。
""")
pydantic_schema = re.sub(r"^ {4}", "", inspect.getsource(AnswerSchema), flags=re.MULTILINE)
example = r"""
问题:
"'万科企业股份有限公司'年报是否宣布了分红政策变更?"
答案:```
{
"step_by_step_analysis": "1. 问题询问是否有分红政策变更。\n2. 年报12、18页提到年度分红金额增加,但政策未变。\n3. 45页有分红细节。\n4. 持续小幅增长,符合既定政策。\n5. 问题问的是政策变更,非金额变化。",
"reasoning_summary": "年报显示分红金额变化但政策未变,答案为False。",
"relevant_pages": [12, 18, 45],
"final_answer": false
}```
"""
system_prompt = build_system_prompt(instruction, example)
system_prompt_with_schema = build_system_prompt(instruction, example, pydantic_schema)
对应 kind = “number” 的prompt:
class AnswerWithRAGContextNumberPrompt:
instruction = AnswerWithRAGContextSharedPrompt.instruction
user_prompt = AnswerWithRAGContextSharedPrompt.user_prompt
class AnswerSchema(BaseModel):
step_by_step_analysis: str = Field(description="""
详细分步推理过程,至少5步,150字以上。
**严格的指标匹配要求:**
1. 明确问题中指标的精确定义,它实际衡量什么?
2. 检查上下文中的所有可能指标。不要只看名称,要关注其实际衡量内容。
3. 仅当上下文指标的含义与目标指标*完全一致*时才接受。可接受同义词,但概念不同则不可。
4. 拒绝(并返回'N/A')的情况:
- 上下文指标范围大于或小于问题指标。
- 上下文指标为相关但非*完全等价*的概念(如代理指标或更宽泛类别)。
- 需要计算、推导或推断才能作答。
- 聚合不匹配:问题要求单一值,但上下文仅有总计。
5. 不允许猜测:如对指标等价性有任何疑问,默认返回`N/A`。
""")
reasoning_summary: str = Field(description="简要总结分步推理过程,约50字。")
relevant_pages: List[int] = Field(description="""
仅包含直接用于回答问题的信息页面编号。只包括:
- 直接包含答案或明确陈述的页面
- 强有力支持答案的关键信息页面
不要包含仅与答案弱相关或间接相关的页面。
列表中至少应有一个页面。
""")
final_answer: Union[float, int, Literal['N/A']] = Field(description="""
答案应为精确的数值型指标。
- 百分比示例:
上下文值:58,3%
最终答案:58.3
特别注意上下文中是否有单位、千、百万等说明,需据此调整答案(不变、加3个零或加6个零)。
如数值带括号,表示为负数。
- 负数示例:
上下文值:(2,124,837) CHF
最终答案:-2124837
- 千为单位示例:
上下文值:4970,5(千美元)
最终答案:4970500
- 如上下文指标币种与问题币种不符,返回'N/A'
示例:上下文值780000 USD,问题要求EUR
最终答案:'N/A'
- 如上下文未直接给出指标,即使可由其他指标计算,也返回'N/A'
示例:问题要求每股分红,仅有总分红和流通股数,不能直接作答。
最终答案:'N/A'
- 如上下文无相关信息,返回'N/A'
""")
pydantic_schema = re.sub(r"^ {4}", "", inspect.getsource(AnswerSchema), flags=re.MULTILINE)
example = r"""
示例1:
问题:
"'万科企业股份有限公司'2022年总资产是多少?"
答案:```
{
"step_by_step_analysis": "1. 问题询问'万科企业股份有限公司'2022年总资产。'总资产'指公司拥有的全部资源。\n2. 年报第78页有'合并资产负债表',列明2022年12月31日总资产。\n3. 该行数据为'总资产',与问题完全匹配。\n4. 报告显示总资产为18500342000元。\n5. 无需计算,直接取值。",
"reasoning_summary": "年报78页直接给出2022年总资产,无需推算。",
"relevant_pages": [78],
"final_answer": 18500342000
}```
示例2:
问题:
"'某医药公司'年报期末研发设备原值是多少?"
答案:```
{
"step_by_step_analysis": "1. 问题询问研发设备原值。\n2. 年报35页有'固定资产净值'12500元,但为净值,非原值。\n3. 37页有'累计折旧'11万元,但未区分研发设备。\n4. 无法直接获得研发设备原值。\n5. 因此答案为'N/A'。",
"reasoning_summary": "年报无研发设备原值,严格匹配应返回N/A。",
"relevant_pages": [35, 37],
"final_answer": "N/A"
}```
"""
system_prompt = build_system_prompt(instruction, example)
system_prompt_with_schema = build_system_prompt(instruction, example, pydantic_schema)
对应 kind = “name” 的prompt:
class AnswerWithRAGContextNamePrompt:
instruction = AnswerWithRAGContextSharedPrompt.instruction
user_prompt = AnswerWithRAGContextSharedPrompt.user_prompt
class AnswerSchema(BaseModel):
step_by_step_analysis: str = Field(description="Detailed step-by-step analysis of the answer with at least 5 steps and at least 150 words. Pay special attention to the wording of the question to avoid being tricked. Sometimes it seems that there is an answer in the context, but this is might be not the requested value, but only a similar one.")
reasoning_summary: str = Field(description="Concise summary of the step-by-step reasoning process. Around 50 words.")
relevant_pages: List[int] = Field(description="""
List of page numbers containing information directly used to answer the question. Include only:
- Pages with direct answers or explicit statements
- Pages with key information that strongly supports the answer
Do not include pages with only tangentially related information or weak connections to the answer.
At least one page should be included in the list.
""")
final_answer: Union[str, Literal["N/A"]] = Field(description="""
If it is a company name, should be extracted exactly as it appears in question.
If it is a person name, it should be their full name.
If it is a product name, it should be extracted exactly as it appears in the context.
Without any extra information, words or comments.
- Return 'N/A' if information is not available in the context
""")
pydantic_schema = re.sub(r"^ {4}", "", inspect.getsource(AnswerSchema), flags=re.MULTILINE)
example = r"""
示例:
问题:
"'南方航空股份有限公司'的CEO是谁?"
答案:```
{
"step_by_step_analysis": "1. 问题询问'南方航空股份有限公司'的CEO。CEO通常是公司最高管理者,有时也称总裁或董事总经理。\n2. 信息来源为该公司的年报,将用来确认CEO身份。\n3. 年报中明确指出张三为公司总裁兼首席执行官。\n4. 因此,CEO为张三。",
"reasoning_summary": "年报明确写明张三为总裁兼CEO,直接回答了问题。",
"relevant_pages": [58],
"final_answer": "张三"
}```
"""
system_prompt = build_system_prompt(instruction, example)
system_prompt_with_schema = build_system_prompt(instruction, example, pydantic_schema)
对应 kind = “names” 的prompt:
class AnswerWithRAGContextNamesPrompt:
instruction = AnswerWithRAGContextSharedPrompt.instruction
user_prompt = AnswerWithRAGContextSharedPrompt.user_prompt
class AnswerSchema(BaseModel):
"""RAG上下文下多实体/名单类答案的结构定义。"""
step_by_step_analysis: str = Field(description="详细分步推理过程,至少5步,150字以上。注意区分实体类型,避免被迷惑。")
reasoning_summary: str = Field(description="简要总结推理过程,约50字。")
relevant_pages: List[int] = Field(description="""
仅包含直接用于回答问题的页面编号。只包括:
- 直接包含答案或明确陈述的页面
- 强有力支持答案的关键信息页面
不要包含仅与答案弱相关或间接相关的页面。
列表中至少应有一个页面。
""")
final_answer: Union[List[str], Literal["N/A"]] = Field(description="""
每个条目需与上下文完全一致。
如问题问职位(如职位变动),仅返回职位名称,不含姓名或其他信息。新任高管也算作职位变动。若同一职位有多次变动,仅返回一次,且职位名称用单数。
示例:['首席技术官', '董事', '首席执行官']
如问题问姓名,仅返回上下文中的全名。
示例:['张三', '李四']
如问题问新产品,仅返回上下文中的产品名。候选产品或测试阶段产品不算新产品。
示例:['生态智能2000', '绿能Pro']
如无信息,返回'N/A'。
""")
pydantic_schema = re.sub(r"^ {4}", "", inspect.getsource(AnswerSchema), flags=re.MULTILINE)
example = r"""
示例:
问题:
"公司有哪些新任高管?"
答案:```
{
"step_by_step_analysis": "1. 问题询问公司新任高管名单。\n2. 年报89页列出新高管签约信息。\n3. 10.9节说明张三为新任总法律顾问,10.10节李四为新任COO。\n4. 综上,张三和李四为新任高管。",
"reasoning_summary": "年报10.9、10.10节明确列出张三、李四为新任高管。",
"relevant_pages": [89],
"final_answer": ["张三", "李四"]
}```
"""
system_prompt = build_system_prompt(instruction, example)
system_prompt_with_schema = build_system_prompt(instruction, example, pydantic_schema)
拥有一个验证集不仅帮助改进了提示词,也使整个系统受益。
我们将所有关键功能配置化,以便衡量它们的实际效果并微调超参数。以下是一些示例配置字段:
@dataclass
class RunConfig:
# 运行流程参数配置
use_serialized_tables: bool = False
parent_document_retrieval: bool = False
use_vector_dbs: bool = True
use_bm25_db: bool = False
llm_reranking: bool = False
llm_reranking_sample_size: int = 30
top_n_retrieval: int = 10
parallel_requests: int = 1 # 并行的数量,需要限制,否则qwen-turbo会超出阈值
team_email: str = "79250515615@yandex.com"
submission_name: str = "Ilia_Ris vDB + SO CoT"
pipeline_details: str = ""
submission_file: bool = True
full_context: bool = False
api_provider: str = "dashscope" #openai
answering_model: str = "qwen-turbo-latest" # gpt-4o-mini-2024-07-18 or "gpt-4o-2024-08-06"
config_suffix: str = ""
通过配置不同的RAG超参数,惊讶地发 现:原来被寄予厚望的表格序列化,不 仅没有改进系统,反而降低了有效性。
系统化方法胜于“神奇方案”:成功并非依赖单一突破性技术,而是通过系统化的流程优化,结合并精细调整多种技术。 这些关键因素包括:
RAG的优化是一个精细化工程问题,RAG问答结果高度依赖对任务细节的深入理解,通过精准微调每个环 节(解析、检索、路由、排序等),即使简单技术也能实现显著效果。
跑通RAG-Challenge,并整理阅读笔记,包括:
阅读笔记的字数任意(可以是200-500字),找到你认为有价值的部分进行理解和整理
TO DO:运行RAG-Challenge-2,并打造自己的RAG系统
打造知识库,将多份投研报告存放到data/stock_data/pdf_reports中

针对知识库进行提问,比如:
中芯国际在晶圆制造行业中的地位如何?其服务范围和 全球布局是怎样的?半导体行业有哪些关键特性,这些特性如何助力中芯国 际发展?中芯国际的营收和利润情况近期有何变化?影响因素是 什么?中芯国际的收入结构有何变化?尤其是在中国大陆和北 美市场的表现如何?美国对中国半导体产业的限制政策对中芯国际有何影响? 中芯国际如何应对?存放到 data/stock_data/questions.json 中
python -m src.pipeline
原有的pipeline中的处理过程复杂,对其进行简化
# 1. 解析PDF报告为结构化JSON,输出到 debug/data_01_parsed_reports
# 同时保存docling原始输出到 debug/data_01_parsed_reports_debug(含大量元数据,后续流程不使用)
print('1. 解析PDF报告为结构化JSON,输出到 debug/data_01_parsed_reports')
pipeline.parse_pdf_reports_sequential()
# 2. 仅在需要表格序列化配置时调用,
# 会在 debug/data_01_parsed_reports 的每个表格中新增 "serialized_table" 字段
print('2. 序列化表格,输出到 debug/data_01_parsed_reports')
pipeline.serialize_tables(max_workers=5)
# 3. 将解析后的JSON规整为更简单的每页markdown结构,输出到 debug/data_02_merged_reports
print('3. 将解析后的JSON规整为更简单的每页markdown结构,输出到 debug/data_02_merged_reports')
pipeline.merge_reports()
# 4. 导出规整后报告为纯markdown文本,仅用于人工复核或全文检索(如 gemini_thinking_config)
# 新文件在 debug/data_03_reports_markdown
print('4. 导出规整后报告为纯markdown文本,仅用于人工复核或全文检索(如 gemini_thinking_config)')
pipeline.export_reports_to_markdown()
改写 pipeline.export_reports_to_markdown()
编写pdf_mineru.py,通过mineru进行文档解析
from src import pdf_mineru
task_id = pdf_mineru.get_task_id(file_name)
print(f"task_id: {task_id}")
pdf_mineru.get_result(task_id)
pdf_mineru.py:
import requests
import time
import zipfile
api_key = 'xxx'
def get_task_id(file_name):
url='https://mineru.net/api/v4/extract/task'
header = {
'Content-Type':'application/json',
"Authorization":f"Bearer {api_key}".format(api_key)
}
pdf_url = 'https://vl-image.oss-cn-shanghai.aliyuncs.com/pdf/' + file_name
data = {
'url':pdf_url,
'is_ocr':True,
'enable_formula': False,
}
res = requests.post(url,headers=header,json=data)
print(res.status_code)
print(res.json())
print(res.json()["data"])
task_id = res.json()["data"]['task_id']
return task_id
def get_result(task_id):
url = f'https://mineru.net/api/v4/extract/task/{task_id}'
header = {
'Content-Type':'application/json',
"Authorization":f"Bearer {api_key}".format(api_key)
}
while True:
res = requests.get(url, headers=header)
result = res.json()["data"]
print(result)
state = result.get('state')
err_msg = result.get('err_msg', '')
# 如果任务还在进行中,等待后重试
if state in ['pending', 'running']:
print("任务未完成,等待5秒后重试...")
time.sleep(5)
continue
# 如果有错误,输出错误信息
if err_msg:
print(f"任务出错: {err_msg}")
return
# 如果任务完成,下载文件
if state == 'done':
full_zip_url = result.get('full_zip_url')
if full_zip_url:
local_filename = f"{task_id}.zip"
print(f"开始下载: {full_zip_url}")
r = requests.get(full_zip_url, stream=True)
with open(local_filename, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
print(f"下载完成,已保存到: {local_filename}")
# 下载完成后自动解压
unzip_file(local_filename)
else:
print("未找到 full_zip_url,无法下载。")
return
# 其他未知状态
print(f"未知状态: {state}")
return
# 解压zip文件的函数
def unzip_file(zip_path, extract_dir=None):
"""
解压指定的zip文件到目标文件夹。
:param zip_path: zip文件路径
:param extract_dir: 解压目标文件夹,默认为zip同名目录
"""
if extract_dir is None:
extract_dir = zip_path.rstrip('.zip')
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(extract_dir)
print(f"已解压到: {extract_dir}")
if __name__ == "__main__":
file_name = '【财报】中芯国际:中芯国际2024年年度报告.pdf'
task_id = get_task_id(file_name)
print('task_id:',task_id)
get_result(task_id)
原代码是对json进行切分,保存到test_set/databases/chunked_reports中
#对 json 文件分块,输出还是 json
def split_all_reports(self, all_report_dir: Path, output_dir: Path, serialized_tables_dir: Optional[Path] = None):
"""
批量处理目录下所有报告(json文件),对每个报告进行文本分块,并输出到目标目录。
如果提供了序列化表格目录,会尝试将表格内容插入到对应页面的分块中。
主要用于后续向量化和检索的预处理。
参数:
all_report_dir: 存放待处理报告json的目录
output_dir: 分块后输出的目标目录
serialized_tables_dir: (可选)存放序列化表格的目录
"""
# 获取所有报告文件路径
all_report_paths = list(all_report_dir.glob("*.json"))
# 遍历每个报告文件
for report_path in all_report_paths:
serialized_tables_path = None
# 如果提供了表格序列化目录,查找对应表格文件
if serialized_tables_dir is not None:
serialized_tables_path = serialized_tables_dir / report_path.name
if not serialized_tables_path.exists():
print(f"警告:未找到 {report_path.name} 的序列化表格报告")
# 读取报告内容
with open(report_path, 'r', encoding='utf-8') as file:
report_data = json.load(file)
# 分块处理,插入表格分块(如有)
updated_report = self._split_report(report_data, serialized_tables_path)
# 确保输出目录存在
output_dir.mkdir(parents=True, exist_ok=True)
# 写入分块后的报告到目标目录
with open(output_dir / report_path.name, 'w', encoding='utf-8') as file:
json.dump(updated_report, file, indent=2, ensure_ascii=False)
# 输出处理文件数统计
print(f"已分块处理 {len(all_report_paths)} 个文件")

对text_splitter.py进行改写,添加 split_markdown_reports 和 split_markdown_file 函数
split_markdown_reports(all_md_dir: Path, output_dir: Path, chunk_size: int = 30, chunk_overlap: int = 5, subset_csv: Path = None)
all_md_dir——md文件目录;output_dir——输出目录;chunk_size——每块最大行数;chunk_overlap——分块重叠行数;subset_csv——可选,文件名到公司名映射表。split_markdown_file(md_path: Path, chunk_size: int = 30, chunk_overlap: int = 5) -> List[Dict]
md_path——markdown文件路径;chunk_size——每块最大行数;chunk_overlap——分块重叠行数。

在pipeline.chunk_reports() 中调用了text_splitter.split_markdown_reports,对文档进行了切分
保存到 stock_data/databases/chunked_reports
Thinking:针对kind=string的问题,如何实现?
添加AnswerWithRAGContextStringPrompt功能
帮我查找,程序中实现了
AnswerWithRAGContextNumberPrompt
AnswerWithRAGContextBooleanPrompt
AnswerWithRAGContextNamesPrompt
ComparativeAnswerPrompt
ComparativeAnswerPrompt
===
在理解这些实现的基础上,我想实现一个
AnswerWithRAGContextStringPrompt,即开放性的问题,回答的是 一段文本



这里对应的kind为 string,类似AnswerWithRAGContextNumberPrompt 对应的kind=number


api_request.py 中添加 schema== "string"
Thinking:如何搭建自己的RAG系统界面?
可以找到一个还不错的RAG系统界面,截屏下来,让Cursor进行参考,使用**streamlit** 或者 gradio
帮我使用streamlit搭建可视化界面,用户提问了一个问题, 后台调用类似 pipeline.process_questions() 默认kind=string,这里只需要单个问题即可,然后回答用 户,在界面上显示。
参考给你的这个界面风格




经过一系列的沟通调试,搭建出来了streamlit界面,但是输出的结果没有正确解析,还需要完善

结合RAG-Challenge,改成自己的RAG系统,包括:
DashScope API KEY,替换原有的openai APIqwen-turbo-latest作为LLM, text_embedding_v1作为embedding模型docling解析data/stock_data/pdf_reportsdata/stock_data/question.json1、准确性
2、速度
3、成本
4、灵活程度
Agentic:
1)Read
2)Find
3) Summary
4) Bad cases
npm install -g @wucai/wucai-code@latest
Q:用大模型解析文档可以吗?
文档解析:
1)大模型 Qwen-VL
通用视觉模型,理解式模型
2)专业工具,比如 Docling, MinerU (推荐)
精确翻译
布局、OCR、公式、表格、图片
Q:哪个工具 OCR识别 图片/照片里文字 准呢?
paddleocr
https://github.com/PaddlePaddle/PaddleOCR
Q:这种 表格里的数据, 是否放到关系型数据库中更好一些?
更好
但是管理上,如何进行转化
JSON是通过 Docling 或者 MinerU 解析器生成的
Q:如果是用云的向量数据库,需要租这么多数据库,成本太高了,有没有其他方式把100家的数据进行隔离?
可以放到一个数据库中,然后对信息进行metadata的标记
先通过metadata进行筛选
Q: 哪种检索方式更适合?
最常用:混合方式
向量、关键词
向量:语义模拟匹配
Query 和 Chunk 进行向量的相似度计算
关键词:精确、但是不够灵活
情况1:文件数量不是很多,或者前期已经进行了筛选,剩余的文件数量不多 => 都可以放到LLM中 (LLM是真正的语义理解)
情况2:文件数量很多,不可能都放到LLM中,需要进行筛选
混合方式、向量、关键词
qwen-agent =>基于关键词的方法进行检索 =>还是比较准
向量的好处 => 灵活 => 但是 chunk信息量比较多,匹配起来不一定准确
Q:有比较完美的 检索方式的策略么?
不一定有
不论用哪种策略,都只是推荐chunk =》 推荐的chunk,可能没有真正得到结果的作用 => LLM意识到后,可以再进行 Agentic RAG
Q:agentic这个词和agent一直不知道有什么区别,能不能稍微说下区别在哪里?
agentic 自主规划,就是agent的概念
agentic rag
vibe coding
LLM Rerank:
1)不足的地方,可能打分第一次和0.7,第二次是0.8
2)好的地方:理解能力很强
因为我们要推荐的chunk,是对回答用户的query有帮助的
embedding 模型,向量相似度 => 模糊查询
*** Small to Big ***
=> 写摘要,用于后续的检索 => 如果这个逻辑,对AI是有价值的,可以链接到更全的代码
LLM的应用,本质上就是 context engineering
Cursor, Claude Code的问题:
1)可能会忘记之前他做过的内容
2)揣测出来一些没有的内容
Cursor, Claude Code如果是写新内容 => 好用
如果是修改已有的内容 => 不一定好用,因为他可能对代码理解不够
如果让AI找Bug,当前出错了,AI修改代码,怎么修改都不对 => 因为AI没有理解当前代码的逻辑,也没有真正找到 Bug为什么出错的原因
找到了可能的原因,还有可能找到 多个不相关的原因
方案1:XXX (推荐)
方案2:XXX
方案3:XXX
我认为方案3更合理
我的解决方案:
Step1,将问题贴给AI,让它查看代码,分析原因以及置信度(0-100),先不修改代码
Step2,只看高分的,比如 95分
梳理当前的代码逻辑,写到 .md中
如果刚才执行了95分的方案,还不对 => 继续找原因以及置信度(0-100)
写的这个 .md (代码逻辑的摘要),如果对于复杂的Bug,可能会修改4-5次
{
"step_by_step_analysis": "1. 问题要求比较多家公司2022年总资产。\n
2. 各公司独立答案:A公司6,601,086,000元,B公司1,249,642,000元,C公司217,435,000元。\n
3. 直接比较得C公司最低。\n
4. 若有公司币种不符则排除。\n
5. 因此答案为C公司。",
"reasoning_summary": "独立答案显示C公司总资产最低,直接得出结论。",
"relevant_pages": [],
"final_answer": "C公司"
}
{
"step_by_step_analysis": "1. 问题要求总结2022年万科企业股份有限公司的主营业务。\n
2. 年报第10-12页详细描述了公司主营业务,包括房地产开发、物业服务等。\n
3. 结合上下文,归纳出主要业务板块。\n
4. 重点突出房地产开发和相关服务。\n
5. 形成简明扼要的总结。",
"reasoning_summary": "年报10-12页明确列出主营业务,答案基于原文归纳。",
"relevant_pages": [10, 11, 12],
"final_answer": "万科企业股份有限公司2022年主营业务包括房地产开发、物业服务、租赁住房、物流仓储等,核心业务为住宅及商业地产开发与运营。"
}
response= {"status_code": 200, "request_id": "33cb8462-eeaa-4bf5-98c5-64b8f3472ab1", "code": "", "message": "", "output": {"text": null, "finish_reason": null, "choices": [{"finish_reason": "stop", "message": {"role": "assistant", "content": "```\n{\n \"step_by_step_analysis\": \"
1. 问题要求总结中芯国际2024年主营业务的主要内容。\\n
2. 根据上下文,第一页明确指出:中芯国 际是世界领先的集成电路晶圆代工企业之一,也是中国大陆集成电路制造业领导者,拥有领先的工艺制造能力、产能优势、服务配套,向全球客户提供8英寸和12英寸晶圆代工与技术服务。\\n
3. 同时,公司还致 力于打造平台式的生态服务模式,提供设计服务与IP支持、光掩模制造等一站式配套服务,并促进产业链上下游协同,为客户提供全方位的集成电路解决方案。\\n
4. 这些信息在多个页面重复强调,如第一页和第六页均明确说明其核心业务为晶圆代工及配套服务。\\n
5. 因此,答案应聚焦于晶圆代工(8/12英寸)和配套服务两大板块。\",\n
\"reasoning_summary\": \"年报多处明确描述了中芯国际的核心业务,包括晶圆代工和配套服务,且数据一致,可直接归纳。\",\n
\"relevant_pages\": [0],\n \"final_answer\": \"中芯国际2024年主营业务主要包括集成电路晶圆代工业务(覆盖8英寸和12英寸技术平台)以及设计服务与IP支持、光掩模制造等一站式配套服务,致力于为客户提供全方位的集成电路解决方案。\"\n}\n```"}}]}, "usage": {"input_tokens": 7673, "output_tokens": 302, "total_tokens": 7975}}
1、o3-mini => qwen-turbo-latest
2、openai text-embedding => qwen text-embedding
3、Docling => MinerU
4、增加 ContextStringPrompt
5、中文数据集
6、界面
Q:steamlit 是和gradio一样的图形界面框架吗?运行streamlit 也必须用这个关键词才行?
是的,python的GUI框架,有streamlit 和 gradio
streamlit run XXX.py
针对这个项目 D:\RAG-cy
运行 streamlit run app_streamlit.py
的时候显示:content 字段不是合法的 JSON 字符串!
===中间打印过程===
[计时] 开始初始化 QuestionsProcessor ...
[计时] QuestionsProcessor 初始化耗时: 0.00 秒
[计时] 开始调用 process_single_question ...
[计时] [单问] 开始公司名抽取 ...
警告:subset.csv 不是 utf-8 编码,自动尝试 gbk 编码...
[计时] [单问] 公司名抽取耗时: 0.00 秒
[计时] [单问] 开始检索与LLM推理 ...
[计时] [get_answer_for_company] 检索器初始化耗时: 0.01 秒
[计时] [HybridRetriever] 开始向量检索 ...
[计时] [HybridRetriever] 向量检索耗时: 0.25 秒
[计时] [HybridRetriever] 开始LLM重排 ...
[计时] [HybridRetriever] LLM重排耗时: 26.01 秒
[计时] [HybridRetriever] 总耗时: 26.26 秒
[计时] [get_answer_for_company] 检索耗时: 26.26 秒
[计时] [get_answer_for_company] 构建rag_context耗时: 0.00 秒
dashscope.api_key= sk-00b7c296f942498cb70c9b021e97b170
model= qwen-turbo-latest
response= {"status_code": 200, "request_id": "33cb8462-eeaa-4bf5-98c5-64b8f3472ab1", "code": "", "message": "", "output": {"text": null, "finish_reason": null, "choices": [{"finish_reason": "stop", "message": {"role": "assistant", "content": "```\n{\n \"step_by_step_analysis\": \"1. 问题要求总结中芯国际2024年主营业务的主要内容。\\n2. 根据上下文,第一页明确指出:中芯国 际是世界领先的集成电路晶圆代工企业之一,也是中国大陆集成电路制造业领导者,拥有领先的工艺制造能力、产能优势、服务配套,向全球客户提供8英寸和12英寸晶圆代工与技术服务。\\n3. 同时,公司还致 力于打造平台式的生态服务模式,提供设计服务与IP支持、光掩模制造等一站式配套服务,并促进产业链上下游协同,为客户提供全方位的集成电路解决方案。\\n4. 这些信息在多个页面重复强调,如第一页和第六页均明确说明其核心业务为晶圆代工及配套服务。\\n5. 因此,答案应聚焦于晶圆代工(8/12英寸)和配套服务两大板块。\",\n \"reasoning_summary\": \"年报多处明确描述了中芯国际的核心业务,包括晶圆代工和配套服务,且数据一致,可直接归纳。\",\n \"relevant_pages\": [0],\n \"final_answer\": \"中芯国际2024年主营业务主要包括集成电路晶圆代工业务(覆盖8英寸和12英寸技术平台)以及设计服务与IP支持、光掩模制造等一站式配套服务,致力于为客户提供全方位的集成电路解决方案。\"\n}\n```"}}]}, "usage": {"input_tokens": 7673, "output_tokens": 302, "total_tokens": 7975}}
content= ```
{
"step_by_step_analysis": "1. 问题要求总结中芯国际2024年主营业务的主要内容。\n2. 根据上下文,第一页明确指出:中芯国际是世界领先的集成电路晶圆代工企业之一,也是中国大陆集成电路制造业领导者,拥有领先的工艺制造能力、产能优势、服务配套,向全球客户提供8英寸和12英寸晶圆代工与技术服务。\n3. 同时,公司还致力于打造平台式的生态服务模式,提供设计服务与IP支持、光掩模制造等一站式配套服务,并促进产业链上下游协同,为客户提供全方位的集成电路解决方案。\n4. 这些信息在多个页面重复强调,如第一页和第六页均明确说明其核心业务为晶圆代工及配套服务。\n5. 因此,答案应聚焦于晶圆代工(8/12英寸)和配套服务两大板块。",
"reasoning_summary": "年报多处明确描述了中芯国际的核心业务,包括晶圆代工和配套服务,且数据一致,可直接归纳。",
"relevant_pages": [0],
"final_answer": "中芯国际2024年主营业务主要包括集成电路晶圆代工业务(覆盖8英寸和12英寸技术平台)以及设计服务与IP支持、光掩模制造等一站式配套服务,致力于为客户提供全方位的集成电路解决方案。"
}```
[计时] [get_answer_for_company] LLM调用耗时: 3.91 秒
警告:subset.csv 不是 utf-8 编码,自动尝试 gbk 编码...
[计时] [get_answer_for_company] 总耗时: 30.18 秒
[计时] [单问] 检索+LLM推理耗时: 30.18 秒
[计时] [单问] 总耗时: 30.19 秒
[计时] process_single_question 推理耗时: 30.19 秒
[计时] answer_single_question 总耗时: 30.19 秒
[DEBUG] step_by_step_analysis: -
[DEBUG] reasoning_summary: -
[DEBUG] relevant_pages: []
[DEBUG] final_answer: -
Stopping...
===
帮我查看代码,先不修改代码
Q:腾讯也有个ima知识库,不知道大家用过没,处理这种财报效果咋样
应该可以,不过我觉得 notebooklm RAG质量可能会更好
Q:那个step_to_step 的推理,是和大模型的think推理有啥区别?
逻辑上都是然大模型进行推理,step by step 这个句子,就是让大家分步骤思考
不过项目中,我们将 step by step具象化了,给了它一些示例
Q:word ppt文档用什么解析
可以用MinerU
Q:那个step_to_step 的推理,是和大模型的think推理有啥区别?好像没有看到类似think那个标签,他是什么样的机制运行的
推理大模型的训练,是鼓励大模型生成 <think></think>标签
enable_thinking = true
小模型 + 微调
大模型