大语言模型LLM

一种通过学习大量的文本来预测整个序列或者下一个词的概率,从而理解和生成自然语言的模型。

大模型发展历程

基于规则与概率:这类模型主要依赖于人工设计的规则和统计方法。如N-gram模型通过计算词语出现的频率来预测下一个词,但存在稀疏性问题,无法很好地处理未见过的词语组合。

神经网络语言模型引入词向量:NNLM利用神经网络,将词语映射成连续的向量(词嵌入),从而捕捉词语间的语义关系。这解决了稀疏性问题,并能更好地理解上下文。然而,NNLM通常需要针对特定任务进行端到端训练。

预训练语言模型:预训练 + 微调以Transformer架构为核心,PLM(如BERT、GPT-½)通过在海量文本上进行无监督预训练来学习通用语言知识,然后针对下游任务进行微调。这极大提高了模型的泛化能力和任务表现

大预言模型:规模化与涌现能力,LLM(如GPT-¾、PaLM)在PLM的基础上,进一步扩大模型规模(参数量、数据量),从而展现出涌现能力,可以执行多样的任务,如问答、摘要、代码生成等,甚至不需要微调(in-context learning)。

阶段 代表技术 特点
基于规则与统计 N-gram模型 简单易解释,但数据稀疏、泛化差
神经网络语言模型 NNLM 引入词向量,解决稀疏性问题
预训练语言模型 BERT, GPT Transformer架构,预训练+微调范式
大语言模型 GPT-3, LLaMA 千亿参数,涌现能力,通用性强

⭐️大语言模型(LLM)核心技术架构

架构 代表模型 适用场景
Encoder-Only BERT 文本理解、分类任务
Encoder-Decoder T5 序列到序列任务(翻译、摘要)
Decoder-Only GPT系列 文本生成、对话任务

LLM训练

阶段 方法 产生的模型类型 特点
预训练 在大规模数据集上进行无监督或自监督学习,学习通用特征 基础模型 (Base) 通用知识强,但不会对齐
监督微调 在特定任务数据集上进行监督学习,优化模型性能。 指令微调模型 (SFT) 能理解指令和任务
对齐与增强 训练奖励模型、用人类反馈做强化学习(如PPO/DPO)让模型更符合人类偏好与安全要求 对齐模型 (Chat/Aligned) 更安全、更符合人类偏好
任务/领域微调 LoRA/PEFT/全参微调 专用模型 (Domain/Task) 适应行业和特定任务

大模型权重的存储

格式分类 (常见扩展名) 一句话概括 主要优点 最适合的场景 关键点 / 备注
PyTorch 格式 (.pth, .pt, .bin) PyTorch框架的“官方标配”,训练和研究时最常用。 灵活,能保存任何东西,在PyTorch生态里用起来最顺手。 模型开发与训练:自己从头开始训练模型或做学术研究。 存在安全风险,加载不可信文件可能执行恶意代码。
Safetensors 格式 (.safetensors) Hugging Face推出的“安全卫士”。 绝对安全,加载速度飞快,是未来的趋势。 分享与下载模型:从网上下载别人训练好的模型时,这是首选。 只存储纯粹的权重数据,不包含任何可执行代码。
量化格式 (.gguf, …q4_0.bin) 模型的“减肥版”,通过降低精度给模型瘦身。 大幅减小模型体积和内存占用,让低功耗设备也能运行。 边缘计算:在手机、树莓派等计算资源非常有限的设备上部署模型。 量化是一种技术,GGUF 是承载这种技术最流行的格式之一。

大模型精度类型

  • 高精度(如FP32)提供更细腻的数值表示,误差小,适合复杂计算。
  • 低精度(如INT8、4-bit)范围受限,量化误差大,适合简单任务或资源受限场景。
  • 混合精度通过高精度梯度弥补低精度权重的不足,整体精度较高。
精度类型 描述 优点 缺点 适用场景 精度 数值范围
FP32(单精度浮点数) 32位浮点数,IEEE 754标准,高精度浮点表示 高精度,广泛硬件支持,适合复杂任务 存储和计算开销大,推理速度慢 科学研究、复杂模型训练 高,约7位十进制精度,误差极小 ±1.18×10⁻³⁸ 至 ±3.4×10³⁸
FP16(半精度浮点数) 16位浮点数,减少一半存储和计算需求 推理速度快,内存占用低,精度损失较小 可能溢出或下溢,需硬件支持 GPU加速推理,内存受限环境 中,约3-4位十进制精度,适合大多数任务 ±6.1×10⁻⁵ 至 ±6.55×10⁴
BF16(Brain Float 16) 16位浮点数,指数范围与FP32相同,精度稍低 保持大动态范围,适合深度学习 精度低于FP16,需专用硬件(如TPU) 深度学习推理与训练,硬件优化 中低,约2-3位十进制精度 ±1.18×10⁻³⁸ 至 ±3.4×10³⁸

⭐️大模型使用

开源大模型使用

1
2
3
4
5
6
pip install openai

Client.chat.completions.create(
model=os.getenv('path'),
messages=[
{'role': 'system', 'content': '你是一个小助手'}, {'role': 'user', 'content': '你是谁?'}])

闭源大模型使用

  • 模型权重 (Model Weights): 加载模型本身参数所需的显存。
    • 参数量(B) × 单个参数大小(Bytes)
  • KV Cache: 存储模型计算过程中的键值对缓存,用于加速生成过程。这是最主要的可变开销。
    • (batch_size × seq_len × num_layers × 2 × hidden_size) × Bytes_per_element
  • 激活值 (Activations): 模型在计算过程中产生的中间张量。
    • 1-2 GB

⭐️语言模型的评估指标

BLEU

BLEU (双语评估替补)分数是评估一种语言翻译成另一种语言的文本质量的指标。

BLEU 通过计算 生成文本参考文本 之间 N-gram 的精确率 (Precision) 来衡量生成文本的“正确性”和“流畅性”。

计算精确率:

  • 分别计算candidate句和reference句的N-grams模型,然后统计其匹配的个数,计算精确率。

  • 公式:candidate和reference中匹配的 n−gram 的个数 /candidate中n−gram 的个数

计算短句惩罚 (Brevity Penalty, BP)。

  • 如果生成文本长度 c 大于等于参考文本长度 r,则 BP=1;否则 $BP = exp(1 - \frac rc)$。exp: 自然指数函数 e^x^。本例Candidate 长度 c = 6,Reference 长度 r = 5,因为 c 大于 r,所以 BP = 1

计算最终 BLEU 分数:
$$
\mathrm{BLEU} = \mathrm{BP} \cdot \exp\left( \sum_{n=1}^{N} w_{n} \log p_{n} \right)
$$

ROUGE

ROUGE指标是在机器翻译、自动摘要、问答生成等领域常见的评估指标。用于衡量信息覆盖度。ROUGE通过将模型生成的摘要或者回答与参考答案(一般是人工生成的)进行比较计算,得到对应的得分。

核心思想:与强调“精确性”的 BLEU 不同,ROUGE 关注的是 召回率 (Recall) 。它衡量的是,一篇 “标准答案文本“(Reference) 中的要点 ,有多少被你的 模型生成的文本(Candidate)给覆盖 了。

PPL

困惑度(Perplexity, PPL)用来度量一个概率分布或概率模型预测样本的好坏程度。

PPL基本思想:一个好的语言模型,在预测下一个词时,应该给予正确答案更高的概率。困惑度衡量的就是模型在预测下一个词时的不确定性或“困惑”程度,本质上是对概率分布质量的评估。困惑度越低,说明模型对句子的预测越有信心,模型性能越好。

LLM架构

LLM大模型架构的基础为Transformer。

LLM分类一般分为三种:自编码模型(encoder)、自回归模型(decoder)和序列到序列模型(encoder-decoder)。

AE自编码模型

AE模型,代表作BERT,其特点为:Encoder-Only

基本原理:是在输入中随机MASK掉一部分单词,根据上下文预测这个词。

AE模型通常用于内容理解任务,比如自然语言理解(NLU)中的分类任务:情感分析、提取式问答。

Bert构成

  • Token Embeddings 是词嵌入张量, 第一个单词是CLS标志, 可以用于之后的分类任务。
  • Segment Embeddings 是句子分段嵌入张量, 是为了服务后续的两个句子为输入的预训练任务。用来区别两种句子,判断两个句子是否是语义相似。
  • Position Embeddings 是位置编码张量, 此处注意和传统的Transformer不同, 不是三角函数计算的固定位置编码, 而是通过学习得出来的。以告诉模型“第 i 个 token 在序列中的位置”。

Bert模型通识

参数 取值
transformer 层数 12
特征维度 768
transformer head 数 12
总参数量 1.15 亿

AE模型总结

优点:BERT使用双向transformer,在语言理解相关的任务中表现很好。

缺点:

  • 输入噪声:BERT在预训练过程中使用 [MASK] 符号对输入进行处理,这些符号在下游的finetune任务中永远不会出现,这会导致 预训练-微调差异 。而AR模型不会依赖于任何被mask的输入,因此不会遇到这类问题。
  • 更适合用于语言嵌入表达, 语言理解方面的任务, 不适合用于生成式的任务

AR自回归模型

AR模型,代表作GPT,其特点为:Decoder-Only

基本原理:从左往右学习的模型,只能利用上文或者下文的信息,比如:AR模型从一系列time steps中学习,并将上一步的结果作为回归模型的输入,以预测下一个time step的值。

AR模型通常用于生成式任务(NLG),在长文本的生成能力很强,比如摘要、翻译或抽象问答。

GPT模型构成

GPT采用单向Transformer模型

image-20250928175649601

GPT模型的特点

模型的一些关键参数为:

参数 取值
transformer 层数 12
特征维度 768
transformer head 数 12
总参数量 1.17 亿

AR模型总结

优点:AR模型擅长生成式NLP任务。AR模型使用注意力机制,预测下一个token,因此自然适用于文本生成。此外,AR模型可以简单地将训练目标设置为预测语料库中的下一个token,因此生成数据相对容易。

缺点:AR模型只能用于前向或者后向建模,不能同时使用双向的上下文信息,不能完全捕捉token的内在联系

Seq2Seq序列到序列

encoder-decoder模型同时使用编码器和解码器。

它将每个task视作序列到序列的转换/生成(比如,文本到文本,文本到图像或者图像到文本的多模态任务)。比如对于文本分类任务来说,编码器将文本作为输入,解码器生成文本标签。

Encoder-decoder模型通常用于需要内容理解和生成的任务,比如机器翻译。

T5模型构成

T5模型结构与原始的Transformer基本一致,除了做了以下几点改动:

  • 作者采用了一种简化版的Layer Normalization,去除了Layer Norm 的bias;将Layer Norm放在残差连接外面。
  • 位置编码:T5使用了一种简化版的相对位置编码,即不使用传统的绝对位置编码,而是采用 相对位置偏置(Relative Position Bias):在计算注意力分数时,模型会根据两个 token 的相对位置差,将其映射到一个离散的 bucket(距离小的精确映射,距离大的对数分桶),并为每个 bucket 学习一个可训练的偏置参数,然后将该偏置直接加到注意力分数矩阵上,从而让模型在不依赖绝对位置嵌入的情况下捕捉相对位置信息,并具备对长文本的泛化能力。

T5 模型在此基础之上引入了相对位置偏置项 $b*_{ij}$,形成新的注意力公式:
$$
\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_*k}} + B\right)V
$$

T5模型的特点

模型的一些关键参数为:

参数 取值
transformer 层数 24
特征维度 768
transformer head 数 12
总参数量 2.2 亿

encoder-decoder模型总结

优点:T5模型可以处理多种NLP任务,并且可以通过微调来适应不同的应用场景,具有良好的可扩展性;相比其他语言生成模型(如GPT-2、GPT-3等),T5模型的参数数量相对较少,训练速度更快,且可以在相对较小的数据集上进行训练。

缺点:由于T5模型使用了大量的Transformer结构,在训练时需要大量的计算资源和时间; 模型的可解释性不足。

提示词工程

清晰的指令

注意:要详细的描述、让模型充当某个角色、使用分隔符标明输入的不同部分、提供例子、指定输出长度

  • 详细的描述:当我们进行模型的提问时,不要描述的太笼统,而是尽量多的提供重要的详细信息或上下文.
    • 不要直接说,”帮我写一封情书”;而是说:”用一些温柔的话语写一封情书,来表达我对你的仰慕和思念。最后,我要求书写字体数要不低于500个字”
  • 让模型充当某个角色:当我们使用大模型时,可以让模型充当一个角色,这样模型会更专业更明确的对你的问题进行回复.
    • 我需要你充当一个AI算法面试官的角色,要求你自主的对我进行AI面试过程中常考的面试题,你可以一次说一个问题,然后我回答完,你再出第二道题
  • 使用分隔符标明输入的不同部分:中括号、XML标签、三引号等分隔符可以帮助划分要区别对待的文本,也可以帮助模型更好的理解文本内容。常用’’’’’’把内容框起来
    • 用20个字符总结由三引号分割的文本。”””文本内容”””
  • 提供例子:本质类似于few-shot leaning。先扔给大模型举例,然后让模型按照例子来输出
    • 按照这句评论文本的格式:’””用户输入文本””‘,帮我创造新的样本
  • 指定输出长度:可以要求模型生成给定目标长度的输出。目标输出长度可以根据单词、句子、段落、要点等的计数来指定。中文效果不明显,同时你给定的长度只是个大概, 多少个字这种肯定会不精准,但是像多少段这种效果相对较好.
    • 用三个段落、30个字符概括由三引号分隔的文本。”””文本内容”””文本参考

文本参考

文本参考目的:基于文本文档,辅助大模型问答,降低模型"幻觉"(一本正经的胡说八道)问题。

  • 根据下文中三重引号引起来的文章来回答问题。如果在文章中找不到答案,请写“我找不到答案”,不要自己造答案。

复杂任务拆分简单子任务

类似于人工,如果你作为领导,让下属一次性完成一个非常大的事,那么出错的概率是很大的,很多大项目也是这样,你甚至无从下手。大模型也是同样的道理。把复杂的任务给拆为更为简单的子任务,大模型会有更好的表现

复杂任务:写一篇关于“人工智能在医学中的应用”的讲稿。

① 先生成提纲; ② 扩展每一部分内容; ③ 检查逻辑与语言流畅度; ④ 统一风格。

给模型”思考”的时间

Chain of Thought (思维链) 就是让大模型在输出答案时,不是直接给出结果,而是显式写出推理过程.先一步一步推理,再得出结论

给模型思考时间,本质为链式思考(Chain-of-Thought, CoT)。目的,让模型think step by step(一步步思考).

比如,直接问你一道数学应用题,你肯定不能立即回答出问题,但是如果给你时间让你一步步分析并计算,那么你就很有可能把题解出来.

借助外部工具

在实际应用中,常常会通过 工具调用(Function Calling) 或 插件化扩展 来增强 ChatGPT 的能力。

  • 联网搜索工具:模型不知道实时信息的问题。
    • 调用搜索引擎 API,获取最新新闻、论文、股市信息。
  • 代码执行工具:需要精确计算或数据处理时,模型自身“算得不准”的问题。
    • 调用 Python 解释器运行数学计算、绘图、数据分析。
  • 数据库 / 知识库工具:模型记忆有限,无法覆盖企业内部数据或特定领域知识。
    • 知识图谱、向量数据库(如 Milvus、FAISS)来存储和检索信息。
  • 外部 API 调用:专业需求,比如天气查询、航班查询、地图导航、医疗工具调用。
    • ChatGPT 通过调用天气 API,告诉用户某地实时气温。

提示技术

Zero-shot、Few-shot、Chain-of-Thought (CoT)、Prompt Chaining、ReAct、Self-Consistency、Tree of Thougths(ToT)、Reflexion。

Zero-Shot

Zero-Shot 提示不提供示例,直接靠模型预训练知识完成任务。像问专家问题,他凭经验直接回答。

Few-Shot

虽然大型语言模型展示了惊人的Zero-Shot 能力,但它们在更复杂的任务上仍然表现不佳。

Few-Shot提示通过 1-3 个参考示例引导模型理解模式,使模型实现了更好的性能。

1
2
3
4
5
6
7
8
9
你是一个翻译专家,请将英文句子翻译成中文。

示例:
英文:I like apples.
中文:我喜欢苹果。

现在请翻译:
英文:The weather is nice today.
中文:

COT

Chain-of-Thought 是一种提示技术,通过展示中间推理步骤来解决复杂问题。这种方法可以帮助模型更好得推理和生成答案。可以将其与少样本提示相结合,以获得更好的结果。

image-20250929164921079

Prompt Chaining

为了提高大语言模型的性能使其更可靠,一个重要的提示工程技术是将任务分解为许多子任务。 确定子任务后,将子任务的提示词提供给语言模型,得到的结果作为新的提示词的一部分。 这就是所谓的链式提示(prompt chaining),一个任务被分解为多个子任务,根据子任务创建一系列提示操作。

链式提示可以完成很复杂的任务。LLM 可能无法仅用一个非常详细的提示完成这些任务。在链式提示中,提示链对生成的回应执行转换或其他处理,直到达到期望结果。除了提高性能,链式提示还有助于提高 LLM 应用的透明度,增加控制性和可靠性。这意味着可以更容易地定位模型中的问题,分析并改进需要提高的不同阶段的性能。

抽取关键信息组织要点,转化为摘要草稿优化摘要

ReAct

ReAct 是一个将推理和行为与 LLMs 相结合通用的范例。ReAct 提示 LLMs 为任务生成口头推理轨迹和操作。这使得系统执行动态推理来创建、维护和调整操作计划,同时还支持与外部环境(例如,Wikipedia)的交互,以将额外信息合并到推理中。

简单来说,ReAct 框架赋予了模型一种“三思而后行”的能力。它将模型的响应过程分解为三个关键部分:

  • Thought (思考): 模型首先会分析当前的任务和已有的信息,进行内在的推理和规划。它会思考“我需要做什么?”、“我缺少什么信息?”、“下一步该怎么办?”。
  • Act (行动): 根据“思考”的结果,模型会决定并执行一个具体的“行动”。这个行动通常是向外部工具(如搜索引擎、数据库、计算器,甚至是你代码中的“知识库”)发起查询,以获取完成任务所需的额外信息。
  • Observation (观察): 模型接收并“观察”执行“行动”后返回的结果。这个结果会成为下一步“思考”的新依据。

这个 思考 -> 行动 -> 观察 的循环会一直持续,直到模型认为自己已经收集到了足够的信息,能够完美地回答用户的问题为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
问题:这个月有几个法定节假日?分别是什么?

开始ReAct推理流程...

模型输出(第1步):
Thought: 为了回答这个问题,我首先需要知道当前的日期来确定是哪一个月。然后,我可以查找该月有哪些法定节假日。
Action: get_current_date
Action Input:

执行工具: get_current_date | 输入:
Observation: 2025年9月26日

模型输出(第2步):
Thought: 现在我知道了当前日期是2025年9月26日,接下来我需要查找2025年9月份有哪些法定节假日。
Action: search_holidays
Action Input: 2025年9月

执行工具: search_holidays | 输入: 2025年9月
Observation: 2025年9月没有法定节假日。

模型输出(第3步):
Thought: 根据之前的观察结果,2025年9月份没有法定节假日。
Action: Final Answer
Action Input: 2025年9月没有法定节假日。因此,这个月的法定节假日数量为0。

任务完成!最终答案:
2025年9月没有法定节假日。因此,这个月的法定节假日数量为0。

Reflexion

自我反思是一种 利用语言反馈来提升语言智能体表现 的方法。它的核心做法是把环境给出的反馈(可能是自由文本,也可能是数值分数)转化为自然语言形式的“反思”,并作为上下文提供给智能体的下一轮推理。这样,智能体就能更快、更有效地从过去的错误中总结经验,从而在复杂任务上表现得更好。

相对于传统的强化学习(RL)或微调需要很多样本、梯度更新、参数训练,Reflexion提供了一种轻量级、可解释的改进agent输出的方法。

image-20250929173658441

如上图所示,自我反思由三个不同的模型组成:

  • 参与者(Actor):根据状态观测量生成文本和动作。参与者在环境中采取行动并接受观察结果,从而形成轨迹。链式思考(CoT) 和 ReAct 被用作参与者模型。此外,还添加了记忆组件为智能体提供额外的上下文信息。
  • 评估者(Evaluator):对参与者的输出进行评价。具体来说,它将生成的轨迹(也被称作短期记忆)作为输入并输出奖励分数。根据任务的不同,使用不同的奖励函数(决策任务使用LLM和基于规则的启发式奖励)。
  • 自我反思(Self-Reflection):生成语言强化线索来帮助参与者实现自我完善。这个角色由大语言模型承担,能够为未来的试验提供宝贵的反馈。自我反思模型利用奖励信号、当前轨迹和其持久记忆生成具体且相关的反馈,并存储在记忆组件中。智能体利用这些经验(存储在长期记忆中)来快速改进决策。

总的来说,自我反思的关键步骤是 a)定义任务,b)生成轨迹,c)评估,d)执行自我反思,e)生成下一条轨迹。

特点

高质量:修正错误,提升精度。

低成本:不需要修改模型参数,成本低。

Self-Consistency

在提示词工程里,自我一致性指的是:不是只生成一个答案,而是让大模型生成多个不同的推理路径。然后对这些推理路径的最终答案进行 投票/聚合,选择出现次数最多或者最合理的答案。这样可以减少“单条思路出错”的风险,提高整体的正确率。

特点:

错误少:多样化推理路径,再进行投票,降低随机错误。

性能强:适合复杂推理任务。

方式一:在提示词中,直接让模型使用不同的思路生成多种答案,并让模型进行选择\

1
2
3
4
5
6
7
8
9
10
11
输入:
你是一个数学老师。请用3种不同的方法来推理这个问题,并分别给出最终答案。
最后请总结:哪个答案出现次数最多,把它作为最终的正确答案输出。
问题如下:
一个商店卖铅笔,每支2元。如果小明有20元,他最多能买多少支铅笔?

输出:
直接除法:20元 ÷ 2元/支 = 10支
逐步累加:通过记录每次购买后剩余金额,共可购买10次,剩余0元
画图法:将20元分成10个2元的段,每段代表一支铅笔,共10支
结果:三种方法都得出相同答案:10支

方式二:结合Prompt Chaining,先生成不同的提示词(风格、思路等不同),然后再去分别生成结果再去手动投票

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Step 1: 生成不同思路的提示词

输入:

你是一个数学老师。请用3种不同的方法来推理这个问题,只需给出推理思路,不需要解答。思路需要简洁明了,并且合理有效。输出格式为:{"思路一":"xxx", "思路二":"xxx", "思路三":"xxx"}
问题如下:
"一个商店卖铅笔,每支2元。如果小明有20元,他最多能买多少支铅笔?"
输出:

{"思路一":"用总钱数除以每支铅笔的单价,所得的商即为最多能购买的铅笔数量,忽略余数。","思路二":"从小明的总金额开始,每次减去一支铅笔的价格,重复此过程直到金额不足购买一支,统计减法执行的次数。","思路三":"列出铅笔数量与总花费的对应关系(如1支2元,2支4元……),找到总花费不超过20元的最大数量。"}
Step 2: 将每个思路拼接成一个prompt,分别调用大模型得到结果

输入:

你是一个数学老师。请用如下的思路来解决这个问题。只输出答案即可。
思路:
"用总钱数除以每支铅笔的单价,所得的商即为最多能购买的铅笔数量,忽略余数。"
问题:
"一个商店卖铅笔,每支2元。如果小明有20元,他最多能买多少支铅笔?"
输出:

10 10 8
Step 3: 根据结果手动投票,选出最终结果

10
总结:示例1和示例2代表了Self-Consistency的两种常见落地方案。示例1依赖单个提示词让模型一次性生成多种思路并自我投票,优点是实现简单、成本低、速度快,但思路差异有限且投票可能不稳定;示例2通过Prompt Chaining先生成不同提示词再独立求解并人工或程序投票,优点是多样性强、结果更稳健,但实现复杂、成本高、耗时长。

Tree of Thoughts (ToT)

ToT 维护着一棵思维树,思维由连贯的语言序列表示,这个序列就是解决问题的中间步骤。使用这种方法,LLM 能够自己对严谨推理过程的中间思维进行评估。LLM 将生成及评估思维的能力与搜索算法(如广度优先搜索和深度优先搜索)相结合,在系统性探索思维的时候可以向前验证和回溯。

核心思想:不只依赖单一路径,而是构建“思维树”探索多个可能的中间步骤,通过大模型生成候选方案,再评估每个方案的可行性,最终找到最优解。

特点

树状探索:像搜索树一样探索不同思路,而不是单一思路

全局优化:避免陷入单一路径的局限,更容易得到高质量解答

image-20250930184352545

要点:

动机:解决语言模型在问题求解中的局限性,特别是对于需要探索、策略先见性或初始决策起关键作用的任务。为克服这些挑战,引入一种新的语言模型推理框架”Tree of Thoughts”(ToT),通过提供具有连贯性的文本单元("thoughts")的探索,使语言模型能够进行有意识的决策过程,考虑多个不同的推理路径并自我评估选择以决定下一步行动。

方法:提出”Tree of Thoughts”(ToT)框架,通过维护一棵思维树,每个思维是一条连贯的语言序列,作为问题求解的中间步骤,实现语言模型的有意识推理过程。通过与搜索算法(如广度优先搜索或深度优先搜索)结合,允许系统性地探索思维树并进行前瞻和回溯。

优势:ToT框架显著提升了语言模型在需要复杂规划或搜索的任务中的问题求解能力。在Game of 24、Creative Writing和Mini Crosswords等任务中,ToT方法的成功率明显高于传统的prompting方法,例如在Game of 24中,使用ToT方法的成功率达到74%。ToT框架提供了一种直观的方式来观察模块,从而增强了模型的可解释性。

Tree of Thoughts”(ToT)框架为语言模型提供了有意识的决策过程,通过探索连贯的文本单元来解决问题,显著提升了语言模型在复杂任务中的问题解决能力。

Prompt金融工程项目

需求

项目任务(三大业务场景):

  • 金融文本分类:将金融文本分成不同类型。思路如下

    • 需要向模型解释什么叫作「文本分类任务」
    • 需要让模型按照我们指定的格式输出
    • 为了让模型知道什么叫做「文本分类」,我们借用 In-context Learning 的方式,先给模型展示几个正确的例子:
  • 金融文本信息抽取:抽取金融文本中的实体。

    • 需要向模型解释什么叫作「信息抽取任务」
    • 需要让模型按照我们指定的格式(json)输出
    • 为了让模型知道什么叫做「信息抽取」,我们借用 In-context Learning 的方式,先给模型展示几个正确的例子:
  • 金融文本匹配:判断两个金融文本是否类似。

    • 需要向模型解释什么叫作「文本匹配任务」
    • 需要让模型按照我们指定的格式输出
    • 为了让模型知道什么叫做「文本匹配任务」,我们借用 In-context Learning 的方式,先给模型展示几个正确的例子:

LLM实现金融文本分类

Few-Shot方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 1 导入必备的工具包
from dotenv import load_dotenv
import openai
import os

# 加载环境变量
load_dotenv()

# 提供所有类别以及每个类别下的样例
class_examples = {
'新闻报道': '今日,股市经历了一轮震荡,受到宏观经济数据和全球贸易紧张局势的影响。投资者密切关注美联储可能的政策调整,以适应市场的不确定性。',
'财务报告': '本公司年度财务报告显示,去年公司实现了稳步增长的盈利,同时资产负债表呈现强劲的状况。经济环境的稳定和管理层的有效战略执行为公司的健康发展奠定了基础。',
'公司公告': '本公司高兴地宣布成功完成最新一轮并购交易,收购了一家在人工智能领域领先的公司。这一战略举措将有助于扩大我们的业务领域,提高市场竞争力',
'分析师报告': '最新的行业分析报告指出,科技公司的创新将成为未来增长的主要推动力。云计算、人工智能和数字化转型被认为是引领行业发展的关键因素,投资者应关注这些趋势'}

sentences = [
"今日,央行发布公告宣布降低利率,以刺激经济增长。这一降息举措将影响贷款利率,并在未来几个季度内对金融市场产生影响。",
"ABC公司今日发布公告称,已成功完成对XYZ公司股权的收购交易。本次交易是ABC公司在扩大业务范围、加强市场竞争力方面的重要举措。据悉,此次收购将进一步巩固ABC公司在行业中的地位,并为未来业务发展提供更广阔的发展空间。详情请见公司官方网站公告栏",
"公司资产负债表显示,公司偿债能力强劲,现金流充足,为未来投资和扩张提供了坚实的财务基础。",
"最新的分析报告指出,可再生能源行业预计将在未来几年经历持续增长,投资者应该关注这一领域的投资机会",
]


# 2 构建函数,进行prompt设计(描述清楚任务及输出格式)
def init_prompts(class_examples):
'''
初始化前置prompt,便于模型做 incontext learning。
:param class_examples:
:return:
'''
class_list = list(class_examples.keys())
pre_prompt = f'你是一个金融专家,需要对输入的金融领域文本进行分析,将类型归类到{class_list},对于不在这四类中的数据,输出‘不清楚类型’。以下是几个示例分类:'
for class_type, example in class_examples.items():
pre_prompt = pre_prompt + f'"""{example}{class_list}里的什么类别?"""' + f'"""{class_type}。"""'
return {'class_list': class_list, 'pre_prompt': pre_prompt}


# 3 构建推理函数
def model_chat(content: str, history=[]) -> str:
"""
调用大模型对话接口
:param messages: 输入内容
:param model: 模型名称
:return: 大模型输出内容 str
"""
client = openai.OpenAI(
base_url=os.environ.get('DASHSCOPE_BASE_URL', ''),
api_key=os.environ.get('DASHSCOPE_API_KEY', '')
)
messages = [{"role": "user", "content": content}]
response = client.chat.completions.create(
model=os.environ.get('DASHSCOPE_CHAT_MODEL', ''),
messages=history + messages,
stream=False,
)
text = response.choices[0].message.content
return text


# 4 构建后处理函数

# 5 调用推理+后处理():
prompts_info = init_prompts(class_examples)
for sentence in sentences:
content = f"{prompts_info['pre_prompt']}"+f'"""{sentence}{prompts_info["class_list"]} 里的什么类别?"""'
print("content: ", content)
resp = model_chat(content, history=[])
print("class result: ", resp)
print('\n')

history方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 1 导入必备的工具包
from dotenv import load_dotenv
import openai
import os

# 加载环境变量
load_dotenv()

# 1 提供所有类别以及每个类别下的样例
class_examples = {
'新闻报道': '今日,股市经历了一轮震荡,受到宏观经济数据和全球贸易紧张局势的影响。投资者密切关注美联储可能的政策调整,以适应市场的不确定性。',
'财务报告': '本公司年度财务报告显示,去年公司实现了稳步增长的盈利,同时资产负债表呈现强劲的状况。经济环境的稳定和管理层的有效战略执行为公司的健康发展奠定了基础。',
'公司公告': '本公司高兴地宣布成功完成最新一轮并购交易,收购了一家在人工智能领域领先的公司。这一战略举措将有助于扩大我们的业务领域,提高市场竞争力',
'分析师报告': '最新的行业分析报告指出,科技公司的创新将成为未来增长的主要推动力。云计算、人工智能和数字化转型被认为是引领行业发展的关键因素,投资者应关注这些趋势'}

sentences = [
"今日,央行发布公告宣布降低利率,以刺激经济增长。这一降息举措将影响贷款利率,并在未来几个季度内对金融市场产生影响。",
"ABC公司今日发布公告称,已成功完成对XYZ公司股权的收购交易。本次交易是ABC公司在扩大业务范围、加强市场竞争力方面的重要举措。据悉,此次收购将进一步巩固ABC公司在行业中的地位,并为未来业务发展提供更广阔的发展空间。详情请见公司官方网站公告栏",
"公司资产负债表显示,公司偿债能力强劲,现金流充足,为未来投资和扩张提供了坚实的财务基础。",
"最新的分析报告指出,可再生能源行业预计将在未来几年经历持续增长,投资者应该关注这一领域的投资机会",
]


# 2 构建函数,进行prompt设计(描述清楚任务及输出格式)
def init_prompts(class_examples):
'''
初始化前置prompt,便于模型做 incontext learning。
:param class_examples:
:return:
'''
class_list = list(class_examples.keys())
pre_history = [{"role": "system",
"content": f'你是一个金融专家,需要对输入的金融领域文本进行分析,将类型归类到{class_list},对于不在这四类中的数据,输出‘不清楚类型’'}]
for class_type, example in class_examples.items():
# pre_history.append((f'{example}是{class_list}里的什么类别?',class_type))
pre_history.append({'role': "user",
'content': f'{example}{class_list}里的什么类别?'
})
pre_history.append({'role': 'assistant',
'content': class_type
})

return {'class_list': class_list, 'pre_history': pre_history}


# 3 构建推理函数
def model_chat(content: str, history=[]) -> str:
"""
调用大模型对话接口
:param messages: 输入内容
:param model: 模型名称
:return: 大模型输出内容 str
"""
client = openai.OpenAI(
base_url=os.environ.get('DASHSCOPE_BASE_URL', ''),
api_key=os.environ.get('DASHSCOPE_API_KEY', '')
)
messages = [{"role": "user", "content": content}]
response = client.chat.completions.create(
model=os.environ.get('DASHSCOPE_CHAT_MODEL', ''),
messages=history + messages,
stream=False,
)
text = response.choices[0].message.content
return text


# 4 构建后处理函数

# 5 调用推理+后处理():
prompts_info = init_prompts(class_examples)
for sentence in sentences:
content = f"“{sentence}”是 {prompts_info['class_list']} 里的什么类别?"
print("content : ", content)
resp = model_chat(content, history=prompts_info['pre_history'])
print("class result : ", resp)

LLM实现金融文本信息抽取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# 1导入必备的工具包
from dotenv import load_dotenv
import openai
import json
import os

# 加载环境变量
load_dotenv()

# 定义不同实体下的具备属性
schema = {
'金融': ['日期', '股票名称', '开盘价', '收盘价', '成交量'],
}

IE_PATTERN = "{}\n\n提取上述句子中{}的实体,并按照JSON格式输出,上述句子中不存在的信息用['原文中未提及']来表示,多个值之间用','分隔。"

# 提供一些例子供模型参考
ie_examples = {
'金融': [
{
'content': '2023-01-10,股市震荡。股票古哥-D[EOOE]美股今日开盘价100美元,一度飙升至105美元,随后回落至98美元,最终以102美元收盘,成交量达到520000。',
'answers': {
'日期': ['2023-01-10'],
'股票名称': ['古哥-D[EOOE]美股'],
'开盘价': ['100美元'],
'收盘价': ['102美元'],
'成交量': ['520000'],
}
}
]
}

sentences = [
'2023-02-15,寓意吉祥的节日,股票佰笃[BD]美股开盘价10美元,虽然经历了波动,但最终以13美元收盘,成交量微幅增加至460,000,投资者情绪较为平稳。',
'2023-04-05,市场迎来轻松氛围,股票盘古(0021)开盘价23元,尽管经历了波动,但最终以26美元收盘,成交量缩小至310,000,投资者保持观望态度。',
]


# 2 构建函数,进行prompt设计(描述清楚任务及输出格式)
def build_prompt(ie_examples):
history_list = [{'role': 'system', 'content': "你是信息提取专家,需要需要完成信息抽取任务。我会给你一个句子,你需要提取句子中的实体,并按照JSON格式输出,如果句子中有不存在的信息用['原文中未提及']来表示,多个值之间用','分隔。"}]

# 遍历示例,将样本和实体 添加到history_list中
for type, example_list in ie_examples.items(): # 获取到金融类型
for example in example_list: # 遍历每个样本
sentence = example['content']
entity_type = ','.join(schema[type]) # 获取到金融类型需要抽取的实体
history_list.append({'role': 'user', 'content': IE_PATTERN.format(sentence, entity_type)})
history_list.append({'role': 'assistant', 'content': json.dumps(example['answers'], ensure_ascii=False)})

# print(f'history_list-->{history_list}')
return {'history_list': history_list}


# 3 构建推理函数
def model_chat(content: str, history=[]) -> str:
"""
调用大模型对话接口
:param messages: 输入内容
:param model: 模型名称
:return: 大模型输出内容 str
"""
client = openai.OpenAI(
base_url=os.environ.get('DASHSCOPE_BASE_URL', ''),
api_key=os.environ.get('DASHSCOPE_API_KEY', '')
)
messages = [{"role": "user", "content": content}]
response = client.chat.completions.create(
model=os.environ.get('DASHSCOPE_CHAT_MODEL', ''),
messages=history + messages,
stream=False,
)
text = response.choices[0].message.content
return text


# 4 构建后处理函数
def clean_response(response: str):
# 将模型输出进行清理, 将 ```json清理掉
if '```json' in response:
response = response.split('```json')[1].split('```')[0]
# print('response-->', response)
# 将这个json字符串转为字典
try:
return json.loads(response) # 防止这步转换出错,使用try except
except:
return response
return response


# 5 调用推理+后处理
prompt_dict = build_prompt(ie_examples)
for sentence in sentences:
print(f'sentence-->{sentence}')
result = model_chat(sentence, prompt_dict['history_list'])
final_result = clean_response(result)
print(f'final_result-->{final_result}')

LLM实现金融文本匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# 1 导入必备的工具包
from dotenv import load_dotenv
import openai
import os

# 加载环境变量
load_dotenv()

# 提供相似,不相似的语义匹配例子
examples = {
'是': [
('公司ABC发布了季度财报,显示盈利增长。', '财报披露,公司ABC利润上升。'),
],
'不是': [
('黄金价格下跌,投资者抛售。', '外汇市场交易额创下新高。'),
('央行降息,刺激经济增长。', '新能源技术的创新。')
]
}

sentence_pairs = [
('股票市场今日大涨,投资者乐观。', '持续上涨的市场让投资者感到满意。'),
('油价大幅下跌,能源公司面临挑战。', '未来智能城市的建设趋势愈发明显。'),
('利率上升,影响房地产市场。', '高利率对房地产有一定冲击。'),
]


# 2 构建函数,进行prompt设计(描述清楚任务及输出格式)
def init_prompts(examples):
"""
初始化前置prompt,便于模型做 incontext learning。
"""
pre_history = [{"role": "system",
"content": '现在你需要帮助我完成文本匹配任务,当我给你两个句子时,你需要回答我这两句话语义是否相似。只需要回答是否相似,不要做多余的回答。'}
]

for key, sentence_pairs in examples.items():
for sentence_pair in sentence_pairs:
sentence1, sentence2 = sentence_pair
pre_history.append({
"role": 'user',
"content": f'句子一: {sentence1}\n句子二: {sentence2}\n上面两句话是相似的语义吗?'
})
pre_history.append({
"role": 'assistant',
"content": key
})

return {'pre_history': pre_history}


# 3 构建推理函数
def model_chat(content: str, history=[]) -> str:
"""
调用大模型对话接口
:param messages: 输入内容
:param model: 模型名称
:return: 大模型输出内容 str
"""
client = openai.OpenAI(
base_url=os.environ.get('DASHSCOPE_BASE_URL', ''),
api_key=os.environ.get('DASHSCOPE_API_KEY', '')
)
messages = [{"role": "user", "content": content}]
response = client.chat.completions.create(
model=os.environ.get('DASHSCOPE_CHAT_MODEL', ''),
messages=history + messages,
stream=False,
)
text = response.choices[0].message.content
return text


# 4 构建后处理函数

# 5 调用推理+后处理
prompts_info = init_prompts(examples)
for sentence_pair in sentence_pairs:
sentence1, sentence2 = sentence_pair
sentence_with_prompt = f'句子一: {sentence1}\n句子二: {sentence2}\n上面两句话是相似的语义吗?'
result=model_chat(sentence_with_prompt, history=prompts_info['pre_history'])
print(f'sentence_pair-->{sentence_pair}')
print(f'result-->{result}')

Day01

1、什么是RAG?有哪些作用?

RAG是一种将大规模语言模型(LLM)与外部知识源的检索相结合,以改进问答能力的工程框架。

作用:缓解LLM“幻觉”问题 : LLM在生成文本时有时会“一本正经地胡说八道”,即生成听起来合理但实际上不准确或捏造的信息,这被称为“幻觉”。RAG通过提供外部事实依据,显著减少了这种幻觉现象,让LLM的输出更具 事实性可靠性

获取最新信息 :LLM的训练数据通常是静态的,这意味着它们无法获取到训练截止日期之后发生的事件或更新的信息。RAG允许LLM连接到实时或定期更新的外部数据源(如新闻、数据库、内部文档等),从而提供 最新、最及时 的答案。

领域特定知识增强 :对于特定行业或企业内部的知识,LLM的通用训练数据往往不足。RAG能够将LLM与企业内部的知识库文档或特定领域的数据连接起来,使LLM能够回答高度专业化的问题,并提供 更符合上下文的答案

降低模型微调成本 :传统上,为了让LLM适应特定任务或数据,需要进行昂贵的 微调(Fine-tuning) 。RAG提供了一种更经济高效的替代方案,它无需修改LLM的底层参数,只需更新外部知识库即可,大大降低了维护和更新模型的成本。

提高答案的可解释性和溯源性 :RAG可以引用其获取信息的来源,这意味着用户可以查看LLM答案所依据的原始文档或数据,增强了答案的 透明度用户信任度

2、RAG标准流程是什么?

image-20251010085034021

RAG 标准流程由索引(Indexing)、检索(Retriever)和生成(Generation)三个核心阶段组成。

  • 索引阶段,通过处理多种来源多种格式的文档提取其中文本,将其切分为标准长度的文本块(chunk),并进行嵌入向量化(embedding),向量存储在向量数据库(vector database)中。

    • 加载文件

    • 内容提取

    • 文本分割 ,形成chunk

    • 文本向量化

    • 将向量存储到向量数据库

  • 检索阶段,用户输入的查询(query)被转化为向量表示,通过相似度匹配从向量数据库中检索出最相关的文本块。

    • query向量化
    • 在文本向量中匹配出与问句向量相似的top_k个
  • 生成阶段,检索到的相关文本与原始查询共同构成提示词(Prompt),输入大语言模型(LLM),生成精确且具备上下文关联的回答。

    • 匹配出的文本作为上下文和问题一起添加到prompt中
    • 提交给LLM生成答案

3、EduRAG的项目流程是怎样的?

image-20251009222655839

4、LangChain中有哪些主要组件,都是用来干什么的?

image-20251009222805521

  • Models:模型,各种类型的模型和模型集成,比如GPT-4
  • Prompts:提示,包括提示管理、提示优化和提示序列化
  • Memory:记忆,用来保存和模型交互时的上下文状态
  • Indexes:索引,用来结构化文档,以便和模型交互
  • Chains:链,一系列对各种组件的调用
  • Agents:代理,决定模型采取哪些行动,执行并且观察流程,直到完成为止

5、目前LangChain支持哪三种类型的模型?

  • LLMs:大语言模型接收文本字符作为输入,返回的也是文本字符。

    • 可以使用OpenAI的形式进行调用;或者langchain社区提供的专用调用方式
  • 聊天模型:基于LLMs, 不同的是它接收聊天消(一种特定格式的数据)作为输入,返回的也是聊天消息。

    • AIMessage、HumanMessage、SystemMessage
    • ChatMessage: Chat 消息可以接受任意角色的参数,但是在大多数时间,我们应该使用上面的三种类型
  • 文本嵌入模型:文本嵌入模型接收文本作为输入, 返回的是浮点数列表。

    • 每个句子或段落最终得到的是一个固定维度的一维列表

6、Prompts组件的使用方式是怎样的?

  • 使用 PromptTemplate 或 ChatPromptTemplate 来定义一个带占位符的模板,

  • 通过 .format() 或 .format_messages() 方法填充具体内容,得到最终的提示词

  • 将提示词送入大模型得到预测结果

7、Chains组件使用方式是怎样的?

使用LLMChain将多个组件组装起来,或者使用SimpleSequentialChain将多个链组装起来,然后调用这个链即可。

或者使用LCEL的方式,使用|符号将不同的组件连接起来,形成一个链式结构。【注意:(1)先后顺序(2)上一个组件的输出类型和下一个组件的输入类型要保持一致】

8、常用的Output Parsers有哪些?

解析器名称 核心功能 输出的 Python 类型 工业级应用场景
StrOutputParser 默认解析器。将 LLM 的输出直接解析为字符串。 str 只需要原始回答时(如问答任务、对话场景)
CommaSeparatedListParser 将 LLM 输出的、用逗号分隔的文本解析为列表。 list[str] 列表/枚举型输出
JsonOutputParser 极其常用。将 LLM 输出的 JSON 字符串解析为 Python 字典。 dict 结构化 JSON 输出
PydanticOutputParser 极其常用。将 LLM 输出解析为预先定义的 Pydantic 对象,提供类型安全和数据验证。 自定义的 pydantic.BaseModel 对象 输出需要严格结构化(JSON-like)数据时
DatetimeOutputParser 从文本中智能地解析出日期和时间信息。 datetime.datetime 需要时间格式时

Day02

1、如何使用LangChain中Memory组件来记录上下文?

方法一:使用 ChatMessageHistory 手动添加上下文

1
2
3
history = ChatMessageHistory()
history.add_user_message("你好") # 添加用户消息
history.add_ai_message("您好") # 添加AI消息

方法二:使用 ConversationChain 自动保存用户和AI的历史交互内容,作为后续回复的上下文

1
2
conversation = ConversationChain(llm=model)
result1 = conversation.predict(input="小明有1只猫")

2、LangChain中常用的文档加载器有哪些?

https://python.langchain.com/v0.2/docs/integrations/document_loaders/

image-20251010205153081

3、常用的文档分割器有哪些?

  • 递归字符文本分割器(RecursiveCharacterTextSplitter)

    • 方法:首先尝试使用第一个分隔符(如 “\n\n”)分割文本,如果分割后的块仍然过大,则使用下一个分隔符继续分割,重复此过程,直到达到指定的 chunk_size 或用完所有分隔符。
    • 优点:比简单的字符分割更能保持语义完整性。
  • 语义文档分割器(SemanticChunker)

    • 方法:基于语义相似性分割文本——先将文本分成语句,然后对语句进行嵌入,再对嵌入的语句计算相似度,相似度差异大的切分成块。
    • 优点:能够更好地保持语义完整性。
  • MarkdownHeaderTextSplitter(Markdown文档切割器)

    • 方法:按照Markdown的标题进行拆分。
    • 优点:基于原始结构进行拆分,语义切分完整。

4、VectorStores是什么?

VectorStores是一种特殊类型的数据库,它的作用是存储由嵌入创建的向量,提供相似查询等功能。

常用的有Faiss、Chroma、Milvus等

5、什么是检索器?

检索器是 LangChain 中负责信息检索的模块,通常与 索引(Indexes) 模块(如向量存储、嵌入模型)结合使用。它的核心功能是:

  • 输入:接收用户查询(通常是文本)。
  • 处理:根据查询从数据源中检索相关内容。
  • 输出:返回一组相关文档或文本片段(通常是 Document 对象列表)。

6、检索器工作流程是什么?

工作流程可以分为以下步骤:

  • 查询嵌入:将用户查询通过嵌入模型(如 OpenAIEmbeddings)转为向量表示
  • 相似性搜索:在向量存储中查找与查询向量最相似的文档向量。
  • 文档返回:返回匹配的文档(包含内容、元数据等)。
  • 后处理(可选):对检索结果进行排序、过滤或重新排名。

7、常见的检索器类型有哪些?

(1)向量检索器VectorStoreRetriever

功能:基于向量数据库来完成相似文档检索

使用方法:

1
2
3
4
5
6
7
8
9
retriever = vector_store.as_retriever(
search_type="similarity", # 可选 "similarity"|"mmr"|"similarity_score_threshold"
search_kwargs={
"k": 5, # 返回结果数量
"score_threshold": 0.7, # 仅当search_type="similarity_score_threshold"时有效,低于阈值的都丢弃。
"filter": {"source": "重要文档.pdf"}, # 元数据过滤,只会检索满足条件的文档。
"lambda_mult": 0.25 # 仅MMR搜索有效(控制多样性):接近 0 则更强调和查询的相关性;接近 1 则更强调结果之间的差异性
}
)

image-20251010210821396

(2)TFIDFRetriever/BM25Retriever

作用:基于词频和逆文档频率进行检索,使用的算法为TF-IDF或BM25。

特点:适合快速构建原型,但不支持语义搜索。

(3)多查询检索器MultiQueryRetriever

作用:它借助 LLM 自动生成多个语义等价的改写查询,把用户的问题扩展成多个角度,然后对每个改写进行检索,最后合并结果,以提高召回率。

特点:提高召回率和精准度,适合复杂查询

(4)集成检索器EnsembleRetriever

作用:结合多种检索器(如 BM25 和向量存储),融合结果。

特点:提高召回率和精准度,适合复杂查询

(5)压缩检索器ContextualCompressionRetriever

作用:对检索结果进行压缩,提取最相关的内容

特点:使用语言模型对检索到的文档进行重新排序或精炼,减少无关内容,提高结果质量

(6)自定义检索器CustomRetriever

作用:开发者可以自定义检索逻辑,适配特定数据源或算法

特点:支持任意场景

8、什么是BM25算法?有什么特点?

BM25 是对 TF-IDF 的改进版本,在 TF-IDF 基础上做了归一化(防止长文档优势)与饱和控制(防止词频无限增长)。

image-20251010211721443

image-20251012083946067

9、Agent是什么?

从大模型的角度来看,Agent其实就是基于大模型的语义理解和推理能力,让大模型拥有解决复杂问题时的任务规划能力,并调用外部工具来执行各种任务,并且能够保留“记忆”的一个智能体。

Agent = 大模型 + 任务规划(Planning) + 使用外部工具执行任务(Tools&Action) + 记忆(Memory)

10、Milvus 是什么?

Milvus 是一款开源的向量数据库(2019年提出),其唯一目标是存储、索引和管理大规模嵌入向量。

11、Milvus中核心的概念有什么?

一个 Milvus 集群最多支持 64 个数据库。

在Milvus数据库中,有Collection和Field的概念,可以和关系数据库中表和字段进行对应:

Milvus 关系数据库 描述
Collection 集合相当于关系数据库中的表,用于组织数据
Field 字段 字段Schema相当于表中的列
Entity 实体就是一条完整的数据记录,包含一个或多个字段
Index 索引 从原始数据衍生出来的重组数据结构,可以大大加快向量相似性搜索的过程
Partition 分区 在物理存储上将 Collections 数据分成多个部分,即分区

注意:1个collection最多支持4个向量Field

12、为什么选择 Milvus?

  • 高性能:性能卓越,可对海量数据集进行向量相似度检索。
  • 高可用、高可靠:Milvus 支持在云上扩展,其容灾能力能够保证服务高可用。
  • 混合查询:Milvus 支持在向量相似度检索过程中进行标量字段过滤,实现混合查询。
  • 开发者友好:支持多语言、多工具的 Milvus 生态系统。

Day03

1、Milvus中常用的索引有哪些类型?原理是什么?有什么特点?

  • FLAT :
    • 原理:暴力计算所有向量与查询向量的距离
    • 特点:精度最高,查询速度最慢,适合数据量小但要求极高精度的场景
  • IVF_FLAT
    • 原理:先聚类划分簇,再在目标簇内暴力搜索
    • 特点:各项能力比较均衡,实现成熟,适用于中等数据规模一般要求场景
  • IVF_SQ8
    • 原理:类似 IVF_FLAT,但向量被压缩存储(8位整数)
    • 特点:相对于IVF_FLAT精度有所下降,但内存占用降低,计算速度加快,适用于大规模数据下一般要求场景
  • IVF_PQ
    • 原理:将向量空间划分为子空间并使用子空间簇的索引对原始向量进行量化
    • 特点:查询速度最快,内存占用最低,适用于超大规模急速查询的场景
  • HNSW
    • 原理:构建多层导航图进行快速近邻搜索
    • 特点:精度非常高,查询非常快,但内存占用较高,适用于超大规模高精度查询场景。

image-20251012205348864

2、Milvus中有哪些相似度度量方法?

对于浮点向量,通常使用以下指标:

  • L2(欧几里得距离):计算向量间的直线距离,值越小越相似,常用于图像处理等领域。
  • IP(内积):计算两个向量的点积,值越大越相似。当向量经过归一化时,其结果等价于余弦相似度,常用于NLP文本向量搜索。
  • COSINE(余弦相似度):衡量两个向量方向的夹角余弦值,值越大(越接近1)表示方向越一致,对向量的幅度不敏感,非常适合文本和语义相似性搜索。

对于稀疏向量,通常使用以下指标:

  • IP(内积):主要用于衡量稀疏向量的相似性。
  • BM25:一种常用于信息检索的评分函数,也支持用于稀疏向量的搜索。

3、Milvus如何实现过滤搜索?

1
2
3
4
5
6
7
8
# 过滤搜索
res = client.search(collection_name='demo_v2',
data=[[0.19886812562848388, 0.06023560599112088, 0.6976963061752597, 0.2614474506242501,
0.838729485096104]],
limit=2, # 返回的个数
search_params={"metric_type": "IP"}, # 查询的参数-指定相似度度量的方式
output_fields=["id", 'vector'], # search_params是在查询时执行距离计算方式,如果定义索引的时候,已经制定了方式可以不写
filter="color like 'red%'")

4、什么叫混合检索?有哪些方式?

混合检索:选择适当的重新排序策略对两组 ANN 搜索结果进行合并和重新排序

方式一:加权排名

  • 原理:对不同的检索结果赋予不同的权重,然后加权求和。
  • 场景:如果用户明确哪些检索结果更重要,想要赋予更高的权重时可以用这种方式。

方式二:倒数排序融合

  • 原理:根据每个结果在其检索列表中的排名位置来计算分数。
image-20251012210154874
  • 场景:如果不清楚哪些检索结果更重要,不进行手动赋权重的时候。

5、如何实现混合检索?

先使用不同的检索方式检索相似文档,然后使用hybrid_search方法进行结果融合,融合时可以使用加权排名或倒数排序融合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# ----------------------------
# 多向量混合搜索(Hybrid Search)示例
# ----------------------------
# hybrid_search() 可在一次请求中同时对多个向量字段执行独立的 ANN(近似最近邻)搜索,
# 每个 AnnSearchRequest 代表一个字段上的查询。
# 搜索结果将通过 ranker(如 RRFRanker)进行融合排序。

# 1)检索路径1:搜索电影向量,使用 L2 距离 进行相似度评估
# 定义针对 filmVector 的搜索请求
query_filmVector = [[0.8896863042430693, 0.370613100114602, 0.23779315077113428,
0.38227915951132996, 0.5997064603128835]]
dense_search_params = {
"data": query_filmVector,
"anns_field": "filmVector", # 与 schema 中定义的字段名一致
"param": {"metric_type": "L2", "nprobe": 10}, # nprobe 控制搜索的簇数量
"limit": 2
}
request_1 = AnnSearchRequest(**dense_search_params)
print(f'Request 1-->{request_1}')

# 2)检索路径2:搜索导演向量,使用 COSINE 进行相似度评估
# 定义针对 posterVector 的搜索请求
query_posterVector = [[0.02550758562349764, 0.006085637357292062,
0.5325251250159071, 0.7676432650114147, 0.5521074424751443]]
sparse_search_params = {
"data": query_posterVector,
"anns_field": "posterVector",
"param": {"metric_type": "COSINE"},
"limit": 2
}
request_2 = AnnSearchRequest(**sparse_search_params)
print(f'Request 2-->{request_2}')

# 3)结果进行聚合
# 第一种融合方式:加权排名策略
rerank = WeightedRanker(0.8, 0.3)
# 使用hybrid_search来实现混合搜索
final_result = client.hybrid_search(collection_name='demo_v3',
reqs=[request_1, request_2],
ranker=rerank,
output_fields=["film_id", "filmVector", "posterVector"],
limit=2)
print(f'final_result-->{final_result}')

# 第二种融合方式:倒数排序融合
ranker = RRFRanker(k=100) # k值默认为60,指的是平滑系数
# 使用hybrid_search来实现混合搜索
final_result = client.hybrid_search(collection_name='demo_v3',
reqs=[request_1, request_2],
ranker=ranker,
output_fields=["film_id", "filmVector", "posterVector"],
limit=2)
print(f'final_result-->{final_result}')

6、你们项目中有没有记录或管理日志?怎么做的?

因为可以根据日志进行代码调试、程序监控、行为审计,所以我们项目记录了日志。

使用的logging这个模块,同时将日志打印到了控制台并记录到了文件中。

7、如何将日志时输出到控制台和文件?

创建一个控制台处理器,再创建一个文件处理器,然后将他们两个添加到日志记录器即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 获取日志记录器
logger = logging.getLogger("Example4")
logger.setLevel(logging.DEBUG)

# 定义日志格式
formatter = logging.Formatter('%(name)s - %(asctime)s - %(levelname)s - %(message)s')

# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
console_handler.setLevel(logging.WARNING) # 每个日志处理器可以单独设置日志级别,但是这个日志级别必须高于或等于处理器级别

# 创建文件处理器
file_handler = logging.FileHandler(filename="C04_base_use.log", encoding="utf-8", mode="a")
file_handler.setFormatter(formatter)
file_handler.setLevel(logging.INFO)

# 将处理器添加到日志记录器中
logger.addHandler(console_handler)
logger.addHandler(file_handler)

Day04

1、rank_bm25模块如何使用?

文档进行分词后送入BM25L或BM250KAPI或BM25Plus训练得到bm25模型,然后将问题进行分词,调用bm25模型的get_scores方法获取每个文档与问题的相似度得分,然后选取最大得分对应的文档索引即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class BM25Search(object):
def __init__(self, docs):
self.docs = docs
# 将文档进行分词
self.tokenized_docs = [jieba.lcut(doc) for doc in self.docs]
# 将分词后的文档送入BM25模型,获得BM25模型的对象
self.bm25 = BM25L(self.tokenized_docs)

def search(self, query):
# 将查询语句进行分词
tokenized_query = jieba.lcut(query)
# 获取BM25模型对象,并调用get_scores方法,传入查询语句,获取查询语句的BM25得分
scores = self.bm25.get_scores(tokenized_query)
# print(f'scores-->{scores}') # [1.09433592 0.22790195]
# 获取最高分数对应的索引
best_index = scores.argmax()
best_score = scores[best_index]
best_doc = self.docs[best_index]
logger.info(f'查询语句:{query},最匹配的文档:{best_doc},BM25得分:{best_score}')

return best_doc, best_score

2、rank_bm25模块中有哪几种模型?

模型 核心改进 优势 适用场景
BM25Okapi 标准版本 理论基础扎实,参数直观 通用文本检索任务
BM25L 对低词频项加平滑(+δ) 改善低频词、短文本表现 新闻、微博、短文本检索
BM25Plus 对整体加平滑(+Δ) 综合性能更稳健,长度惩罚更合理 通用任务、长短文混合场景

3、Redis是干嘛的?在项目中有什么用?

Redis(Remote Dictionary Server)是一个高性能的键值对数据库,常用于缓存、会话管理等场景。

在项目中作用:用户问题到达时,会首先在Redis中检索是否有相同问题,如果有则立即返回对应的答案,从而加快问题回复的效率。其中Redis存储的是极高频问答对(在MySQL中查询出的,与问题相似度阈值>=0.85且有答案的问题及答案)。

4、如何实现对Redis的操作?

通过redis包下的StrictRedis方法创建客户端,然后基于这个客户端实现增删改查操作。

1
2
3
4
5
self.client = redis.StrictRedis(host=self.conf.REDIS_HOST,
port=self.conf.REDIS_PORT,
password=self.conf.REDIS_PASSWORD,
db=self.conf.REDIS_DB,
decode_responses=True) # 将响应解码为字符串

5、如何实现的基于MySQL的FQA系统?

  1. 数据存储:MySQL存储FQA高频问答对数据。
  2. 缓存管理:先基于Redis缓存,返回相同问题的答案,如果没有命中则进行MySQL问题检索;Redis中仅缓存相似度>0.85且有答案的数据。
  3. 问题检索:如果Redis中没有命中相同问题,则使用BM25计算与所有问题的相似度,将相似分数Softmax归一化。获取最相似文档的分数,并判断是否大于阈值0.85,如果是则认为是同一问题,去MySQL中查询该问题的答案;如果不是则调用RAG系统检索。
  4. 答案返回
    • 在MySQL中根据问题查询答案,若返回可靠答案,直接返回。
    • 否则,调用RAG系统检索。
image-20251015143818433

6、MySQL在项目中起什么作用?怎么用的?

MySQL用于存储高频问答对,基于缓存的问题训练BM25模型,用于检索与查询问题相似的问题。然后根据相似问题来查询对应的答案。

注意:

高频问答对的来源——由后端工程师每天统计咨询问题的top50,去重后导入MySQL

拿到这些高频问答对之后,如何插入到MySQL?——

遍历高频问答对中的每个问题,将这个问题进行分词后,送入到训练好的BM25模型(由历史高频问题训练得到的)中,使用BM25计算与所有历史问题的相似度,将相似分数Softmax归一化。获取最相似文档的分数,并判断是否大于阈值0.85,如果是则认为是同一问题,则不再向MySQL中进行插入;否则,则作为新问题插入到MySQL中。

Day05

1、详细描述一下如何使用BM25对MySQL中的高频问答对进行检索?

首先从Redis中读取缓存的所有分词后的高频问题(如果没有则从MySQL中读取所有问题再进行分词),然后送入BM25进行训练得到BM25模型。然后当用户的问题到达时,将问题进行分词后,调用模型的get_scores方法获取用户问题与所有高频问题的相似度得分。再对这个分数进行归一化处理,获取最高分数及对应的索引。如果这个分数大于了0.85,则根据索引获取原始问题,再从MySQL中检索该问题对应的答案;如果这个分数没有大于0.85或者在MySQL中没有检索到答案,则返回None,下一步进行RAG检索。

2、当MySQL的数据更新之后,如何更新Redis中的数据,进而更新BM25模型?

可以对Redis中 用于记录所有历史问题和所有分词问题的key ,即 “qa_original_questions” 和”qa_tokenized_questions”设置一个有效期,有效期可以设置为3小时,这样redis中的这2个key的数据会定期进行删除。当redis中这2个key的数据进行删除之后,就可以按照代码从MySQL中重新进行加载,从而实现它的更新。

3、RAG系统的工作流程是怎样的?

  1. 查询分类
    • 系统首先判断查询类型(如“通用知识”或“专业咨询”)。
    • 通用知识直接由大语言模型回答,专业咨询进入检索流程。
  2. 策略选择
    • 根据查询特点选择检索策略:
      • 直接检索:适用于明确查询。
      • HyDE检索:适用于抽象问题,生成假设答案后检索。
      • 子查询检索:分解复杂查询。
      • 回溯检索:简化复杂问题后检索。
  3. 文档检索
    • 使用vector_store.py从向量数据库中检索相关文档。
    • 支持稠密向量和稀疏向量的混合检索,结果经过重排序优化。
  4. 生成回答
    • 将检索到的文档作为上下文,结合用户查询输入大语言模型,生成自然语言回答。
    • 若大模型调用报错则联系人工。
image-20251015192046702

4、什么是OCR?你们项目中用的哪个框架?

OCR 技术,全称是光学字符识别(Optical Character Recognition),是一种将图像中的文字内容进行识别并提取的技术。

我们项目使用的是PaddleOCR,它是由百度飞桨(PaddlePaddle)团队基于其深度学习框架 PaddlePaddle 开发并开源的一个全流程、超轻量、高精度的 OCR 工具库。

5、你们项目是如何来加载pdf的?

主要思路:

  • 继承LangChain中的BaseLoader类,实现init方法和lazy_load方法,将整个 pdf 的提取结果加上元数据信息作为一个 Document yield 出去。

  • 内容提取的核心逻辑是:使用PyMuPDF模块中的fitz加载PDF,用来提取文字和图片元数据信息,对于图片信息使用PaddleORC(使用的rapidocr_paddle 模块下的 RapidOCR类)识别图片中的文字,最终将结果拼接到一起。

主要流程:

  1. 使用 fitz.open() 打开 PDF。
  2. 逐页 (page) 处理。
  3. 使用 page.get_text() 提取原生文本。
  4. 使用 page.get_image_info(xrefs=True) 获取页面上的图片信息。
  5. OCR 应用: 对获取到的图片,检查其尺寸是否超过预设阈值 PDF_OCR_THRESHOLD(默认为页面宽高的 60%)。仅对大于阈值的图片执行 OCR。
  6. 处理页面旋转 (page.rotation),确保 OCR 时图像方向正确。
  7. 调用 get_ocr() 获取的 OCR 实例识别图片文字。
  8. 合并原生文本和 OCR 结果。

6、你们项目是如何来加载doc的?

主要思路:

  • 继承LangChain中的BaseLoader类,实现init方法和lazy_load方法,将整个 word文档 的提取结果加上元数据信息作为一个 Document yield 出去。

  • 内容提取的核心逻辑是:使用python-docx模块加载word文档,然后获取该文档的块信息(Paragraph或Table),接下来去处理每一个块。如果是段落,先把段落中的文字提取出来,然后段落中的图片使用PaddleORC(使用的rapidocr_paddle 模块下的 RapidOCR类)识别其中的文字;如果是表格,则直接遍历获取表格单元格中的信息。最终将结果拼接到一起。

主要流程:

  1. 使用 docx.Document() 打开 DOCX 文件。
  2. 定义 iter_block_items 辅助函数,用于统一遍历文档中的段落 (Paragraph) 和表格 (Table) 块。
  3. 遍历所有块:
  4. 如果是段落,提取 block.text。同时,使用 XPath (.//pic:pic, .//a:blip/@r:embed) 查找并提取段落内嵌入的图片。对提取的图片执行 OCR。
  5. 如果是表格,遍历所有单元格 (cell),提取单元格内段落的文本。
  6. 合并所有提取的文本和 OCR 结果。

7、你们项目ppt加载器怎么做的?

主要思路:

  • 继承LangChain中的BaseLoader类,实现init方法和lazy_load方法,将整个 ppt 的提取结果加上元数据信息作为一个 Document yield 出去。

  • 内容提取的核心逻辑是:使用python-pptx模块加载ppt,然后逐张处理PPT。在处理PPT时,首先将PPT上的形状 (shape) 按视觉顺序(top, left 坐标)排序,排序完后依次去处理每个形状。在处理形状时,如果是文本框 ,则直接提取文本;如果是表格,则直接遍历获取表格单元格中的信息;如果是图片,则使用PaddleORC(使用的rapidocr_paddle 模块下的 RapidOCR类)识别其中的文字;如果是组合形状,则进行递归调用。最终将结果拼接到一起。

主要流程:

  1. 使用 pptx.Presentation() 打开演示文稿。
  2. 逐张幻灯片 (slide) 处理。
  3. 顺序处理: 将幻灯片上的形状 (shape) 按视觉顺序(top, left 坐标)排序。
  4. 定义 extract_text 递归函数处理单个形状:
    • 提取文本框 (shape.has_text_frame) 的文本。
    • 提取表格 (shape.has_table) 内所有单元格的文本。
    • 如果形状是图片 (shape.shape_type 13),提取图片数据 (shape.image.blob`),执行 OCR。
    • 如果形状是组合 (shape.shape_type 6),递归调用 extract_text` 处理其包含的子形状。
  5. 遍历排序后的形状,调用 extract_text
  6. 合并所有提取的文本和 OCR 结果。

8、你们项目图片加载器怎么做的?

主要思路:

  • 继承LangChain中的BaseLoader类,实现init方法和lazy_load方法,将整个图片的提取结果加上元数据信息作为一个 Document yield 出去。
  • 内容提取的核心逻辑是:使用PaddleORC(使用的rapidocr_paddle 模块下的 RapidOCR类)识别其中的文字。

主要流程:

  1. 接收图像文件路径 img_path
  2. 调用 get_ocr() 获取 OCR 实例。
  3. 直接对图像文件执行 OCR。
  4. 将 OCR 结果(所有识别出的文本行)合并成一个字符串。

Day06

1、你们项目用到了哪些文本切分器?怎么切分的?

  • 文本切分器的选择:根据文档的类型进行选择——如果是markdown,则使用MarkdownTextSplitter进行切割;如果是一大段文本,没有明确的段落标识,比如从网上爬取到的信息,则使用AliTextSplitter;其他使用ChineseRecursiveTextSplitter进行切割。
  • 文本切块的方式:先切分了父块,然后在每个父块里边切分了子块。在子块中保存了元数据信息,包括父块的id,路径,父块的内容!最后返回的是所有的子块,需要将子块进行embedding后,存入Milvus中。

2、为什么要进行父块和子块的区分?

  • 如果块特别大,这个时候块中会包括大量跟问题无关的信息,在检索时,没有办法精准找到我们想要的数据。如果将文档切分成了一个一个的小块,可以提高检索时的精准性。但是有一个新的问题,因为块比较小,所以块的上下文信息是比较缺失的,在回答问题时,可能语义不完整。
  • 所以解决方式是:大块切分和小块切分进行了融合,即使用父块和子块组合切分的方式。具体来说,先切分父块,然后在每个父块里边切分了子块,然后在子块中保存父块的内容。最终存储到向量数据时,是将子块进行embedding后进行存储。在检索的时候,就是用子块进行检索,此时可以实现精准检索。当检索到子块后,会将该子块元数据信息中的父块内容进行返回,用于问答,从而解决了上下文缺失问题!

image-20251018095618518

3、为什么自定义中文递归文本切分器?怎么做的?

原因:将长文本按照预设的中文分隔符递归地切分成指定大小的块。

主要实现逻辑:继承langchain.text_splitter.RecursiveCharacterTextSplitter,对分割符列表进行了修改,修改成包括常见的中文标点和换行符,如 ["\n\n", "\n", "。|!|?", "\.\s|\!\s|\?\s", ";|;\s", ",|,\s"]。这有助于在切分时尽量保持中文句子的完整性。

4、为什么自定义基于模型的语义切分器?怎么做的?

原因:通过达摩院开源的文档语义分割模型实现对文本的切分,准确度高并且效率高

主要实现逻辑:继承langchain.text_splitter.CharacterTextSplitter,对split_text方法进行了修改。修改的方式就是利用预训练的文档语义分割模型对文本进行切分。具体的切分方式调用 modelscope.pipeline 加载指定的文档分割模型(nlp_bert_document-segmentation_chinese-base,达摩院开源的文档语义分割模型)模型,将输入文本传递给模型 pipeline 进行处理,最后解析模型输出,得到分块的结果。

为什么选用这个模型?

因为 nlp_bert_document-segmentation_chinese-base 是 达摩院开源的文档语义分割模型,通过自适应滑动窗口的序列模型,对输入文本进行语义层面的段落分割。它先将整个文本作为序列输入,然后使用使用 BERT 基础模型预测每个 token 是否是段落边界,通过自适应滑动窗口动态调整处理窗口大小,提高准确性和效率。

5、项目中如何做的Embedding?

使用milvus_model模块下的BGEM3EmbeddingFunction类加载bge-m3模型,对文本进行嵌入,同时生成用于语义相似度计算的稠密向量用于关键词匹配的稀疏向量,进而做混合检索,提高检索的准确率和召回率。

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from milvus_model.hybrid import BGEM3EmbeddingFunction

ef = BGEM3EmbeddingFunction(model_name_or_path='./bge-m3', use_fp16=True, device="cuda:0")

texts = ["什么是人工智能?", "iPhone 价格是多少?"]
embeddings = ef(texts)

print(embeddings.keys())
print(f"ef.dim['dense']-->{ef.dim['dense']}") # 1024
print(f"ef.dim['sparse']-->{ef.dim['sparse']}") # 250002

# 获取稠密向量
print(f"第一个文本的稠密向量-->{embeddings['dense'][0]}")
# 获取Milvus格式的稀疏向量
sparse = embeddings['sparse']._getrow(0)
sparse_dict = {index:value for index, value in zip(sparse.indices, sparse.data)}
print(f"第一个文本的稀疏向量-->{sparse_dict}")

运行结果:

image-20251018095656008

6、稠密向量和稀疏向量是如何存储和构建索引的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 添加稠密向量字段,FLOAT_VECTOR 类型,维度由嵌入函数指定
schema.add_field(field_name="dense_vector", datatype=DataType.FLOAT_VECTOR, dim=self.dense_dim)
# 添加稀疏向量字段,SPARSE_FLOAT_VECTOR 类型,这个类型是专门用来用于存储稀疏向量数据的
schema.add_field(field_name="sparse_vector", datatype=DataType.SPARSE_FLOAT_VECTOR)

# 创建索引参数对象
index_params = self.client.prepare_index_params()
# 为稠密向量字段创建索引
index_params.add_index(field_name="dense_vector",
index_name="dense_index",
index_type="IVF_SQ8",
metric_type="IP",
# 设置簇的个数:设置簇的个数越多,每个簇中向量数越少,查询的速度变化越快,但是查询的准确度越低
# 一般来说设的值是 根号下文档数
params={"nlist": 10})
# 为稀疏向量字段创建索引
index_params.add_index(field_name="sparse_vector",
index_name="sparse_index",
# 设置索引类型为稀疏倒排索引
index_type="SPARSE_INVERTED_INDEX",
metric_type="IP"
)

7、重排序模型是干什么的?为什么需要重排序?

重排序模型是检索增强生成(RAG)流程中的关键组件,负责对初步检索的大量候选文档进行精确排序,从而提高下游任务的质量。

需要重排序的原因:

(1)使用不同的检索方式检索到的候选文档过多,会超过大模型的输入限制,需要进行重排后,选取最相关topn文档送到大模型中进行内容生成

(2)有论文实验证明,在将相似文档输入给大模型时,如果按相关性由高到低的顺序输入,效果会优于其他顺序,所以需要将模型进行重排序后再输入给大模型。

8、重排序模型用的是什么模型?为什么?

用的BGE-Reranker系列中的bge-reranker-large,它采用交叉编码器(cross-encoder)架构,能够直接计算查询和文档之间的相关性得分,具有强大的语义理解能力,在重排序任务中表现优异。

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from sentence_transformers import CrossEncoder

# 加载模型
reranker = CrossEncoder('./bge-reranker-large', device='cuda:0')

queries = ["什么是机器学习?"]
documents = [
"机器学习是一种让计算机从数据中自动学习规律的方法。",
"深度学习是机器学习的一个分支。",
"今天的天气很好。"
]

# 输入格式必须是二维列表:[[query, document]]
pairs = [[queries[0], doc] for doc in documents]
scores = reranker.predict(pairs)
print(f'scores-->{scores}')

# 输出排序结果
for doc, score in sorted(zip(documents, scores), key=lambda x: x[1], reverse=True):
print(f"{score:.4f} - {doc}")

运行结果:

1
2
3
4
scores-->[9.9982554e-01 3.1844237e-01 7.6328361e-05]
0.9998 - 机器学习是一种让计算机从数据中自动学习规律的方法。
0.3184 - 深度学习是机器学习的一个分支。
0.0001 - 今天的天气很好。

Day07

1、项目中如何实现的混合检索和重排序?

  • 首先使用BGE-M3将问题编码成稠密向量和稀疏向量。
  • 然后为稠密向量和稀疏向量分别创建检索请求,其中稠密向量使用的相似度度量方式为IP,nprobe设置为50【子块总数量为50w, nlist为700,nprobe为50】。稀疏向量使用的相似度度量方式也是IP。在检索时,将学科类型作为过滤条件。
  • 使用client.hybrid_search()做结果融合,混合检索方式为加权排名,稠密向量和稀疏向量的权重分别是1和0.7
  • 根据混合检索得到的子块的元数据信息,获取对应的父块内容。对父块内容进行去重后,使用CrossEncoder加载的bge-reranker-large进行重排序。最后返回topk个重排序后的父块数据。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#  定义方法,实现混合检索和重排序
def hybrid_search_with_rerank(self, query, top_k=conf.RETRIEVAL_K, source_filter=None):
'''
:param query: 查询的问题
:param top_k: 检索返回的topk个文档
:param source_filter: 进行查询过滤的条件,这里指的是学科
:return:
'''
# todo: 1.对查询进行embedding
query_embeddings = self.embedding_function([query])
# 获取稠密向量
dense_vector = query_embeddings["dense"][0]
# print(f'dense_vector-->{dense_vector}') # 维度是1024
# 获取稀疏向量
sparse_vector = query_embeddings["sparse"]._getrow(0)
sparse_dict = {index: value for index, value in zip(sparse_vector.indices, sparse_vector.data)}
# print(f'sparse_dict-->{sparse_dict}')

# todo: 2.构建稠密向量和稀疏向量的检索请求
filter_expression = f"source ` '{source_filter}'" if source_filter else ""
# 创建稠密向量的搜索请求
dense_search_params = {
"data": [dense_vector],
"anns_field": "dense_vector", # 要查询的字段
# nprobe 控制搜索的簇数量,nprobe 值越小,搜索速度越快,但是搜索的准确度越低,通常是nlist的1%-10%
"param": {"metric_type": "IP", "nprobe": 2},
"limit": top_k,
"expr": filter_expression
}
request_1 = AnnSearchRequest(**dense_search_params)
# 创建稀疏向量的搜索请求
request_2 = AnnSearchRequest(
data=[sparse_dict],
anns_field="sparse_vector",
param={"metric_type": "IP"},
limit=top_k,
expr=filter_expression
)

# todo: 3.使用 WeightedRanker 融合结果
# 创建 WeightedRanker 对象,设置权重为稠密向量1.0和稀疏向量0.7
ranker = WeightedRanker(1.0, 0.7)
# 进行混合检索。因为检索结果是一个列表,所以需要取第一个元素
results = self.client.hybrid_search(collection_name=self.collection_name,
reqs=[request_1, request_2],
ranker=ranker,
limit=top_k,
output_fields=["text", "parent_id", "parent_content", "source", "timestamp"])[0]
# print(f'results-->{type(results), results}')
# 对结果进行处理,封装成 Document 对象
sub_chunks = [self._doc_from_hit(hit['entity']) for hit in results]
# print(f'sub_chunks-->{len(sub_chunks), sub_chunks}')

# todo: 4.对去重后的父块进行重排序
# 从子块中去重父块内容,并进行去重
parent_docs = self._get_unique_parent_docs(sub_chunks)
# print(f'parent_docs-->{len(parent_docs), parent_docs}')

# 对父块进行重排序
if parent_docs:
# 数据对为 查询和文档
data_pairs = [(query, doc.page_content) for doc in parent_docs]
scores = self.reranker.predict(data_pairs)
# print(f'scores-->{scores}')
# 根据得分,从高到低去排序文档
ranked_parent_docs = [doc for score, doc in sorted(zip(scores, parent_docs), reverse=True)]
# print(f'ranked_parent_docs-->{ranked_parent_docs}')

# 最终返回排序后的父块文档,返回前 conf.CANDIDATE_M 个
self.logger.info(f"Milvus中已找到 {len(ranked_parent_docs)} 个去重父块,最终返回 {conf.CANDIDATE_M} 个父块")
return ranked_parent_docs[:conf.CANDIDATE_M]
else:
self.logger.warning(f"在Milvus中没有找到去重父块")
return []

2、如果在文档检索时,发现有效文档的召回率较低,怎么处理?

(1)可以优先修改在检索时nprobe的个数,nprobe的个数越大,检索的簇越多,检索的精度越高。

(2)可以修改召回的数量,召回的数量越大,有效文档的召回率越高。

(3)修改或者调整重排序模型【有条件可以微调这个重排序模型】,使有效文档的排名更靠前,从而避免被截取掉。

(4)可以尝试在构建索引时,调低nlist的个数。因为nlist越小,单个簇中的向量数越多,在检索时暴力搜索的向量越多,检索的精度越高。

(5)尝试适当调整索引类型,如果使用的是IVF_PQ,可以调整为更高精度的IVF_SQ8或HNSW。如果数据量少,还可以尝试IVF_FLAT或FLAT。

3、查询分类是干嘛的,怎么做的?

查询分类负责将用户查询分为“通用知识”和“专业咨询”两类,以决定查询路由是直接由大模型生成(通用知识),还是进行RAG检索(专业咨询)。

做法:使用BertForSequenceClassification加载并训练bert_base_chinese模型,得到最终的模型。

4、使用过Trainer训练模型吗?如何使用?

使用过。

需要先配置TrainingArguments参数,然后实例化Trainer对象,将模型、训练参数、训练数据集、验证数据集以及评估函数进行传入,调用Trainer对象的train方法进行训练。

其中TrainingArguments常用的配置参数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 5)模型训练-设置训练参数
training_args = TrainingArguments(
output_dir="./bert_results", # 检查点的保存路径
num_train_epochs=3, # 训练轮数
per_device_train_batch_size=8, # 训练批次大小
per_device_eval_batch_size=8, # 评估批次大小
warmup_steps=50, # 预热步数
weight_decay=0.01, # 使用adamW的权重衰减系数
logging_dir="./bert_logs", # 日志保存路径:如果想生成这个文件夹,需要安装tensorboard
logging_steps=10, # 每10个批次打印一次日志
evaluation_strategy="epoch", # 评估策略,这里选择每个epoch评估一次
save_strategy="epoch", # 保存策略,这里选择每个epoch保存一次
load_best_model_at_end=True, # 在训练结束时加载最佳模型
save_total_limit=1, # 只保存1个检查点,注意这个检查点不一定是最优的模型
metric_for_best_model="eval_loss", # 评估最优模型的指标 eval_loss是自带的,如果使用自定义的指标,需要在compute_metrics方法中进行定义
greater_is_better=False, # 明确指定loss指标越小越好
fp16=False, # 使用混合精度训练(GPU 支持时)
)
# 模型训练-创建Trainer对象
trainer = Trainer(
model=self.model, # 模型
args=training_args, # 训练参数
train_dataset=train_dataset, # 训练数据集
eval_dataset=val_dataset, # 验证数据集
compute_metrics=self.compute_metrics, # 自定义的评估指标
)
# 模型训练-开始训练
self.logger.info("开始训练模型...")
trainer.train()

# 6)保存模型
self.save_model()

Day08

1、详细叙述RAG系统工作流程是如何来实现的?

image-20251021032402042

如何实现的查询改写策略选择?

在我们项目中,主要是通过写提示词的方式,告诉大模型每种策略的含义、适用的场景,并通过示例进行说明。然后让大模型根据输入的问题输出对应的策略。

后期准备构建一些数据集,然后微调大模型,从而实现更加精准的策略选择。

对于子查询检索策略具体怎么实现的?

首先将原始问题交给大模型,让大模型基于子查询的方式将原始问题拆分成几个子问题。然后对于每个子问题分别对Milvus向量数据库进行混合检索和重排序,得到每个子问题对应的排序后的去重的父块内容。然后再根据父块的ID进行去重,得到最终需要的父块内容。最后将父块文档内容进行拼接,再使用RAG的提示词模版和问题进行拼接,一起送到大模型中得到答案。

2、查询改写使用了哪些策略?有什么区别?

  • 直接检索
    • 描述:对用户查询直接进行检索,不进行任何增强处理。
    • 适用场景:适用于查询意图明确,可以直接从知识库中进行检索的问题。
  • 假设问题检索(HyDE)
    • 描述:使用 LLM 生成一个假设的答案,然后基于假设答案进行检索。
    • 适用场景:适用于查询较为抽象,直接检索效果不佳的问题,可以先生成一个近似的答案,再去检索这个答案相似的文档,效果更好。
  • 子查询检索
    • 描述:将复杂的用户查询拆分为多个简单的子查询,分别检索并合并结果。
    • 适用场景:适用于查询涉及多个实体或方面,需要将原始问题拆分成几个子问题,分别检索不同子问题的相似问题,然后进行汇总。
  • 回溯问题检索
    • 描述:将复杂的用户查询转化为更基础、更易于检索的问题,然后进行检索。
    • 适用场景:适用于查询较为复杂,需要简化后才能有效检索的问题。
  • 历史会话改写检索
    • 描述:结合前后文信息,对查询的问题进行改写,将缺失的信息进行补齐。
    • 适用场景:在对话中,前后文是相互关联的。如果仅凭当前的query进行检索,可能会导致召回精度大幅下降,因为query中往往缺少重要的上下文信息。

Day09

1、RAG系统评估的数据集是如何构造的?

流程如下:

  • 使用大模型生成测试样本,具体来说是使用Milvus中存储好的分块文档(子块内容),基于这些文档逆向生成问题和答案 ,这些数据构成了评估系统的基础。
  • 使用大模型对测试样本进行质量审核,确保数据具备清晰性和可检索性。
  • 使用RAG系统对测试样本进行处理,记录检索文档和最终答案,连同问题和真实答案,构造评估数据集,用于评估系统。
  • 使用大模型或评估框架对RAG系统性能进行量化评估。

评估数据集格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
{
"question": "人工智能就业课的课程版本是什么?",
"context": ["人工智能学科全新升级——人工智能开发V6.0课程。"],
"answer": "人工智能就业课的课程版本是V6.0。",
"ground_truth": "V6.0"
},
{
"question": "课程的一句话概括是什么?",
"context": ["解锁「大模型」 新技能成就「高薪AI」人才"],
"answer": "课程的一句话概括是:解锁「大模型」新技能成就「高薪AI」人才。",
"ground_truth": "解锁「大模型」新技能成就「高薪AI」人才。"
}
]

2、RAGAS评估框架中有哪些指标?什么作用?

指标 阶段 作用 谁和谁 通俗理解
上下文相关性 检索 衡量检索到的文档是否全和原始问题有关系 文档与问题 开卷资料带没带对
上下文召回率 检索 衡量真实答案能否全从检索到的文档中推出来 真实答案与文档 开卷资料带没带全
忠实度 生成 衡量模型生成的答案能否全从检索到的文档中推出来 模型答案与文档 是不是根据开卷资料里回答的
答案相关性 生成 衡量模型生成的答案是否和原始问题有关系 模型答案与问题 有没有跑题(是否抄对资料)

具体计算方式:

  • 上下文相关性:将文档和问题交给LLM,由LLM统计与问题相关的句子数量,然后除以总的句子数量。
  • 上下文召回率:将真实答案拆分成几个事实点,然后由LLM判断每个事实点是否能由文档推出,召回率等于能推出的事实数/总的事实数。
  • 忠实度:从模型答案生成几个陈述,然后由LLM判断每个陈述是否能由文档推出,忠实度等于能推出的陈述数/总的陈述数。
  • 答案相关性:使用LLM对模型答案生成几个问题,然后使用embedding模型获取所有问题的嵌入,然后计算原始问题与生成问题的平均余弦相似度作为答案相关性。

3、你们企业中RAGAS的指标是多少?

一般企业中这几个指标的范围

context_recall:86%左右

context_precision:78%左右

faithfulness:91%左右

answer_relevancy:92%左右

注意:

  • context_precision和context_recall是负相关的。一般会牺牲context_precision来换取高的context_recall。
  • answer_relevancy是答案相关性,不等同于答案正确率,答案正确率由context_recall、faithfulness、answer_relevancy共同决定。答案正确率大概在75%。
  • faithfulness和answer_relevancy也是有负相关性的,只是没有那么强烈。在选择时,如果上下文检索的特别好,就可以提高faithfulness;如果上下文检索的不好,就提高answer_relevancy。

4、叙述一下如何将基于MySQL的FQA系统和基于Milvus的RAG系统进行整合?

分别实例化BM25Search对象和RAGSystem对象,调用BM25Search对象的search方法进行缓存查询,如果拿到了缓存答案则直接进行返回;如果没有拿到缓存答案,则调用RAGSystem对象的generate_answer方法进行RAG检索;如果用户的问题有问题,则返回默认答案。

5、说一下项目是如何来做会话管理的?

建库建表:项目使用MySQL来存储每个用户的会话信息,将用户的ID、SessionID、问题、答案与时间等信息存储到MySQL中。

数据更新:每次当问答系统生成答案时,将问题和对应的答案更新到MySQL中。

使用:在使用大模型生成答案时,会从MySQL中获取最近5轮对话,将其拼接到提示词中,然后送入大模型。

6、如何让大模型进行流式输出?在使用流式输出的时候需要注意什么?

在调用大模型时,将stream参数设置成true,即可进行流式输出。

需要注意的点是:流式输出的结果是一个迭代器,需要使用遍历的方式进行获取,如果需要将结果进行返回,则使用yield进行返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def call_dashscope_stream(self, prompt):
if not self.client: # 检查客户端是否可用
logger.error("LLM 客户端未初始化,无法调用 call_dashscope")
return f"错误: LLM客户端不可用"
# 调用 DashScope API
try:
# 创建聊天完成请求
completion = self.client.chat.completions.create(
model=conf.LLM_MODEL,
messages=[
{"role": "system", "content": "你是一个有用的助手。"},
{"role": "user", "content": prompt},
],
temperature=0.1,
stream=True, # 启用流式输出
timeout=30 # 设置超时时间为30秒
)
# 遍历流式输出的每个chunk
for chunk in completion:
if chunk.choices and chunk.choices[0].delta.content:
# 使用yield语句返回每个chunk的content部分
yield chunk.choices[0].delta.content

except Exception as e:
# 记录 API 调用失败
self.logger.error(f"DashScope API 调用失败: {e}")
yield f'错误:LLM API 调用失败 {e}'

7、FastAPI和Flask的联系和区别是什么?

  • 联系:

    它们都是 Python Web 框架,用来创建 API 或 Web 应用。都能接收 HTTP 请求,根据 URL 路由到相应的处理函数,然后返回 HTTP 响应。

  • 区别:

    Flask的核心机制是同步阻塞 ,并发能力较低,适合CPU 密集型任务 (需要持续计算)。

    而FastAPI 的核心机制是异步非阻塞,并发能力高,适合I/O 密集型任务 (大量等待时间,如网络、磁盘、LLM)。

8、在利用大模型部署时怎么选择?

强烈推荐使用 FastAPI (异步)

原因:LLM 调用是典型的需要大量耗时的操作;异步能极大提高并发处理能力;FastAPI 基于 Pydantic 进行自动的数据校验和文档生成,方便处理数据。

Agent-Day01

1、什么是Function Call?

概念:大模型基于具体任务,智能决策何时需要调用某个函数,同时返回符合函数参数的 JSON对象。

能力获得的方式:基于训练来得到的,所以并不是所有大模型都具有Function Call能力。

优势:信息实时性、数据局限性、功能扩展性。

2、Function Call 工作原理是什么?

主要步骤:

  • 用户(客户端)发送请求和提示词,聊天服务器(Chat Server)将该提示词以及当前可调用的函数列表一并发送给大模型。
  • 大模型根据提示词的内容和上下文,判断应生成普通文本回复,还是以函数调用的格式进行响应。
  • 如果模型决定调用函数,它会返回一个包含函数名称和参数的结构化调用指令;聊天服务器接收到该指令后,执行对应的函数,并将函数的实际执行结果返回给大模型。
  • 模型再根据函数返回的数据,将其整合并生成一段自然、连贯的文本作为最终回复,返回给用户。
image-20251025082636706

3、Function Call的使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# todo: 第一步:定义工具函数
@tool
def add(a: int, b: int) -> int:
"""
将数字a与数字b相加
Args:
a: 第一个数字
b: 第二个数字
"""
return a + b

@tool
def multiply(a: int, b: int) -> int:
"""
将数字a与数字b相乘
Args:
a: 第一个数字
b: 第二个数字
"""
return a * b

# 定义 JSON 格式的工具 schema
tools = [add, multiply]

# todo: 第二步:初始化模型
llm = ChatOpenAI(base_url=conf.base_url,
model=conf.model_name,
api_key=conf.api_key,
temperature=0.2)
# 绑定工具,允许模型自动选择工具
llm_with_tools = llm.bind_tools(tools, tool_choice="auto")


# todo: 第三步:调用回复
query = "2+1等于多少?"
# 使用列表的方式来存储对话信息
messages = [HumanMessage(query)]

try:
# todo: 第一次调用
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
print(f"\n第一轮调用后结果:\n{messages}")

# todo: 处理工具调用
# 判断消息中是否有tool_calls,以判断工具是否被调用
if hasattr(ai_msg, "tool_calls") and ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
# 基于工具名称获取对应的函数
func = {"add": add, "multiply": multiply}[tool_call["name"].lower()]
# 解析参数,并调用对应的函数【因为原始的函数使用了注解,现在就是一个被langchain封装后的方法,调用时需要使用invoke进行调用】
tool_result = func.invoke(tool_call["args"])
# 将工具调用结果添加到messages中,用于下一次调用
messages.append(ToolMessage(content=tool_result, tool_call_id=tool_call["id"]))
print(f"\n工具调用结果添加到messages后:\n{messages}")

# todo: 第二次调用,将工具结果传回模型以生成最终回答
final_response = llm_with_tools.invoke(messages)
print(f"\n最终模型响应:\n{final_response.content}")
else:
print("模型未生成工具调用,直接返回文本:")
print(ai_msg.content)
except Exception as e:
print(f"调用失败:{e}")

4、什么是MCP协议?

MCP(Model Context Protocol,模型上下文协议)是由 Anthropic 在2024年1月提出的一套开放协议,旨在实现大型语言模型(LLM)与外部数据源和工具的无缝集成,用来在大模型和数据源之间建立安全双向的链接。也就是说,将这些外部数据源和工具进行统一管理,以方便大模型进行统一调用。

5、MCP 工具调用流程

  • 客户端连接多个 MCP Server,获取并缓存工具清单。

  • LLM 通过这些清单“知道”有哪些可用工具。

  • LLM 根据用户请求决定调用哪个工具,并发出调用指令。

  • MCP Client 把指令转发给对应 Server,Server 执行工具逻辑。

  • 结果返回给 Client,再传回 LLM,生成最终回答。

6、MCP的通信传输方式有哪些?

Stdio:本地进程间通信,部署简单、延迟低,适合私有化或本地化场景;缺点是不能跨网络使用。

SSE(Server-Sent Events):通过 HTTP 长连接单向推送服务器事件,适合实时通知;但需要维护长连接,对横向扩展和断连重连有额外设计成本。

Streamable HTTP:基于单个 HTTP 端点同时支持请求—响应(双向)与流式传输,集成云/负载均衡器更方便,灵活性和工程友好度使其成为当前主流选择。

7、服务端和客户端使用示例

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from mcp.server.fastmcp import FastMCP

# 创建 MCP 实例,指定服务名称、日志级别、主机和端口
mcp = FastMCP("sdg", log_level="ERROR", host="127.0.0.1", port=8001)

@mcp.tool(
name="query_high_frequency_question",
description="从知识库中检索常见问题解答(FAQ),返回包含问题和答案的结构化JSON数据。",
)
async def query_high_frequency_question() -> str:
return "高频问题是: 恐龙是怎么灭绝的?"

if __name__ ` "__main__":
mcp.run(transport="streamable-http") # 使用 streamable-http 传输方式

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from mcp.client.streamable_http import streamablehttp_client
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_openai import ChatOpenAI
from mcp import ClientSession
import asyncio
from agent_learn.config import Config
conf = Config()

# 创建模型
llm = ChatOpenAI(base_url=conf.base_url,
api_key=conf.api_key,
model=conf.model_name,
temperature=0.1)

# MCP Server URL
server_url = "http://localhost:8001/mcp"


# 创建一个异步函数,来实现客户端的创建及使用
async def main():
global mcp_client
# 启动 MCP server,并通过标准输入输出建立异步连接。
async with streamablehttp_client(url=server_url) as (read, write, _):
# 使用读写流创建MCP会话对象
async with ClientSession(read, write) as session:
# 初始化session
await session.initialize()

# 从 session 自动获取 MCP server 提供的工具列表。
tools = await load_mcp_tools(session)

# 创建prompt模板
# agent_scratchpad 这个参数是agent在进行推理的时候,自动生成的,用于记录agent的推理过程。
prompt_template = ChatPromptTemplate.from_messages([
("system", "你是一个乐于助人的助手,能够调用工具回答用户问题。"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])

# 创建agent对象
agent = create_tool_calling_agent(llm, tools, prompt_template)
# 使用agent对象创建agent执行器
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# agent调用
query = "北京天气如何?"
response = await agent_executor.ainvoke({"input": query})
print(f"response-->{response}")

return # 终止进程

# 启动运行
if __name__ ` '__main__':
asyncio.run(main())

Day02

1、用python_a2a包如何创建MCP服务器和客户端?

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import uvicorn
from python_a2a.mcp import FastMCP
from python_a2a.mcp import create_fastapi_app

# 创建 MCP 服务器实例
mcp = FastMCP(
name="MyMCPTools",
description="提供高频问题和天气查询工具",
version="1.0.0"
)


@mcp.tool(
name="get_weather",
description="查询天气"
)
async def get_weather(**kwargs) -> str:
return '{"status": "success", "data": "北京的天气是多云"}'

# 启动服务器
app = create_fastapi_app(mcp)
uvicorn.run(app, host="0.0.0.0", port=8010)

客户端(agent调用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from mcp.client.streamable_http import streamablehttp_client
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_openai import ChatOpenAI
from mcp import ClientSession
import asyncio

from python_a2a import MCPClient, to_langchain_tool

from agent_learn.config import Config

conf = Config()

# 创建模型
llm = ChatOpenAI(base_url=conf.base_url,
api_key=conf.api_key,
model=conf.model_name,
temperature=0.1)


# 创建一个异步函数,来实现客户端的创建及使用
async def main():
# 创建MCP客户端
url = "http://localhost:8010"
mcp_client = MCPClient(server_url=url)

try:
# 获取可用的工具列表
tools = await mcp_client.get_tools()

# 将 MCP tool 转成 LangChain 的工具
get_weather_tool = to_langchain_tool(url, "get_weather")
query_high_frequency_question = to_langchain_tool(url, "query_high_frequency_question")
tools = [get_weather_tool, query_high_frequency_question]

# 创建prompt模板
# agent_scratchpad 这个参数是agent在进行推理的时候,自动生成的,用于记录agent的推理过程。
prompt_template = ChatPromptTemplate.from_messages([
("system", "你是一个乐于助人的助手,能够调用工具回答用户问题。"),
("human", "{user_input}"),
("placeholder", "{agent_scratchpad}"),
])

# 创建agent对象
agent = create_tool_calling_agent(llm, tools, prompt_template)
# 使用agent对象创建agent执行器
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# agent调用
query = "北京天气如何?"
response = await agent_executor.ainvoke({"user_input": query})
except Exception as e:
print(f"Error: {e}")


# 启动运行
if __name__ ` '__main__':
asyncio.run(main())

2、什么是Agent?

Agent就是基于大模型的语义理解和推理能力,让大模型拥有解决复杂问题时的任务规划能力,并调用外部工具来执行各种任务,并且能够保留“记忆”的一个智能体。

Agent = 大模型 + 任务规划(Planning) + 使用外部工具执行任务(Tools&Action) + 记忆(Memory)

3、Agent有哪几种常见的模式?

(1)⼯具使⽤模式

允许 Agent 调用外部工具来弥补自身知识的不足。

agent会自动完成工具的选择和调用,并基于工具调用结果进行最终答案生成。

(2)ReAct 模式

将“思考”(Reasoning)和 “行动”(Acting)紧密地结合在一起,形成一个动态的循环。这个模式让Agent不再是简单地调用工具,而是像人类一样“边想边做”,从而解决更复杂的问题。

**工作流程:**思考——》行动——》行动输入——》观察——》循环迭代

(3)反思模式

Agent 在完成一个步骤或整个任务后,对其结果进行评估生成反馈【大模型自身、评估模型、调用工具、用户反馈】, 然后Agent根据反馈结果进行反思并对结果进行修正。

(4)规划模式

先将一个大目标分解成一个详细的、有序的计划(Plan),然后再逐一执行计划中的每个步骤(每个步骤可能是一个 ReAct 循环)。

(5)多智能体模式

可以设计多个具有不同角色和能力的 Agent,让它们协同工作来完成极复杂的任务。

4、项目中用到了哪些模式?

一个真正强大的 Agent 系统,并不会只使用其中一种模式。它会根据任务的复杂性,灵活地将这些模式组合起来。例如,一个 Agent 面对一个复杂问题时,可能会先启动 规划模式 来分解任务,然后将子任务交给一个使用 ReAct 模式 的执行者,而这个执行者在执行过程中又会调用各种 工具 ,并在遇到困难时启动 反思模式 来修正自己的策略。

image-20251026085535465

5、什么是A2A协议?

A2A协议就是不同智能体进行沟通协作的协议。

作用:

  • 安全协作(可以保证agent之间的交互信息是安全的)
  • 任务与状态管理(提交一个任务后,可以跟踪任务的状态和处理结果)
  • 用户体验协商(智能体可以根据用户的问题和反馈进行调整,提高用户的体验)
  • 能力发现(agent通过AgentCard 来展示自己的功能,其他agent可以自动读取AgentCard 中的信息来了解该智能体的功能)

6、Agent2Agent 核心概念有哪些?

AgentSkill:AgentSkill 定义了单个智能体(Agent)所具备的、可被外部调用的具体功能或能力。

AgentCard:AgentCard 是描述一个智能体身份、能力(AgentSkill)、接口信息和元数据的标准化声明文件,用于代理发现和服务注册。

Task:Task 指的是具体的需要完成目标,会包含关于session_id、状态、任务的内容、处理结果等信息。

TaskState:TaskState是任务状态枚举类,定义了任务的可能状态,包括SUBMITTED/COMPLETED等。

TaskStatus:TaskStatus 表示 A2A 任务的当前状态对象,包括状态枚举(TaskState)、附加消息和时间戳。

A2AServer:A2AServer是A2A协议的核心实现类,用于 构建代理服务器 。

artifacts:artifacts 是 A2A 协议中 Task 对象的核心字段之一,用于存储任务执行后的输出产物(结果)。

AgentNetwork:AgentNetwork 是 A2A 协议中的agent网络管理类,用于集中管理和发现 A2AServer。

AIAgentRouter:AIAgentRouter 是负责根据任务需求和 AgentCard 信息,将任务路由到最合适智能体的组件。