diff --git a/notes/大模型应用.md b/notes/大模型应用.md
index 88b864d2..51dc8e0b 100644
--- a/notes/大模型应用.md
+++ b/notes/大模型应用.md
@@ -80,6 +80,8 @@
* [A24 MCP 服务端的最小实现](#a24-mcp-服务端的最小实现)
* [A25 并发执行工具调用](#a25-并发执行工具调用)
* [A26 停止前校验](#a26-停止前校验)
+ * [A27 Embedding 的池化与批内负样本](#a27-embedding-的池化与批内负样本)
+ * [A28 Spring AI 的对话记忆与工具调用](#a28-spring-ai-的对话记忆与工具调用)
* [参考资料](#参考资料)
@@ -430,6 +432,14 @@ LoRA 的优点:
需要注意的是,LoRA 减少的主要是显存,而不是计算量:前向和反向传播仍然要经过完整的 W,梯度也要穿过每一层传回去,只是不为 W 计算和保存参数的梯度。
+LoRA 属于参数高效微调(Parameter-Efficient Fine-Tuning, PEFT)。这类方法都冻结原模型、只训练少量新增的参数,区别在于参数加在哪里:
+
+- Adapter:在每层中插入一个先降维再升维的小网络。在 BERT 上只增加 3.6% 的参数,GLUE 上的效果与全参数微调只差 0.4%,但它是串行插入的额外层,不能合并进原矩阵,推理时增加延迟;
+- 软提示(Soft Prompt):在输入前拼接若干个可训练的向量,称为虚拟 Token。Prompt Tuning 只加在输入层,模型达到几十亿参数后才接近全参数微调;Prefix Tuning 在每一层都加,只训练约 0.1% 的参数;P-Tuning 用一个小网络生成这些向量。它们不改动模型的权重,但要占用上下文长度;
+- DoRA:把每个权重矩阵分解为大小和方向两部分,方向用 LoRA 更新,大小单独训练,训练后同样能合并,论文中在多项任务上优于 LoRA。
+
+LoRA 最常用,是因为它训练后可以合并进原矩阵,既不增加推理延迟,也不占用上下文。
+
#### 4. QLoRA 与全参数微调
QLoRA 进一步把冻结的原始参数量化为 4 位存储,计算时临时还原为 BF16,梯度只流向 LoRA 参数:
@@ -448,6 +458,13 @@ Qwen2.5-7B 的权重在 4 位下约 4 GB,比 BF16 的 15.2 GB 少了 11 GB 左
全参数微调一张卡放不下时要用多张卡。DeepSpeed 的 ZeRO 和 PyTorch 的 FSDP 把优化器状态、梯度和参数切分到各张卡上,例如 Qwen2.5-7B 在 8 张卡上训练时,全部切分后每张卡的模型状态从 121.8 GB 降到 15.2 GB,见[进阶:ZeRO 与 FSDP](大模型应用%20-%20进阶.md#1-zero-与-fsdp)。
+LoRA 微调的效果不好时,按以下顺序检查:
+
+- 数据:先抽查样本的质量,确认训练时的对话模板与推理时一致、只对回答计算损失,见上文的 SFT 数据;
+- 学习率:LoRA 的最优学习率约为全参数微调的 10 倍,QLoRA 对 7B 模型使用 2 × 10-4,照搬全参数微调的学习率(例如 LIMA 用的 1 × 10-5)会学得过慢;
+- 作用范围和秩:只作用在注意力上时改为全部线性层;数据量大、与预训练差异大时,r 太小会使训练损失降到一定程度后不再跟随全参数微调下降,可以增大 r,仍然不够就改用全参数微调;
+- 过拟合:训练损失持续下降而验证集上变差,减少轮数,增加数据的多样性。
+
#### 5. 灾难性遗忘
灾难性遗忘(Catastrophic Forgetting)指的是模型在新数据上训练之后,原有的能力明显下降,例如只用客服对话微调之后,写代码和多轮推理的能力变差,也就是常说的“微调之后变傻了”。对 1B 到 7B 模型的持续微调实验中普遍出现了遗忘,而且模型越大遗忘越严重。
@@ -640,6 +657,8 @@ Agent 的幻觉除了回答错误,还会出现在工具调用的过程中。
模型只能看到上下文窗口里的内容,回答的质量很大程度上取决于窗口里放了什么。上下文工程(Context Engineering)指的是在每次调用之前,决定把哪些内容、以什么顺序放进窗口,包括系统规则、工具定义、检索到的资料、对话历史和当前问题。
+提示词工程(Prompt Engineering)关注的是怎样写好指令,尤其是系统提示词;上下文工程的范围更大,提示词只是窗口中的一部分,还要管理工具定义、检索结果、对话历史和记忆。Agent 每执行一步都会产生新的工具结果,窗口中的内容一直在变化,每次调用之前都要重新挑选和压缩,这是写好一段固定的提示词解决不了的。
+
#### 1. 放什么、按什么顺序
@@ -833,6 +852,10 @@ Word、HTML、Markdown 文件本身记录了标题样式、表格和列表,按
Embedding 模型把一段文本转为一个固定长度的向量,例如 1024 维。训练时让意思相近的文本的向量互相靠近、无关的文本的向量互相远离,因此向量之间的距离可以衡量语义的相似程度。
+向量的计算过程是:分词之后输入一个 Transformer 编码器,每个 Token 得到一个向量,再把这些向量汇总成一个,称为池化(Pooling),通常再归一化为长度 1。例如 bge-small-zh-v1.5 把“怎么申请年假”切成 [CLS]、怎、么、申、请、年、假、[SEP] 共 8 个 Token,最后一层输出 8 个 512 维的向量,取 [CLS] 位置的向量作为整句的表示。
+
+池化的方式由模型决定:BERT 类的编码器取 [CLS] 位置的向量或者所有 Token 的平均值;Qwen3-Embedding 这类由解码器改成的模型取最后一个 Token 的向量,因为在因果掩码下只有它看到了整句话。使用时要与模型训练时的方式一致,bge-small-zh-v1.5 改用平均值之后,下文“怎么申请年假”与“休假的流程是什么”的相似度从 0.603 变为 0.547(见[附录 A27](#a27-embedding-的池化与批内负样本))。
+
常用余弦相似度衡量两个向量方向的接近程度:cos(a, b) = a · b / (|a| |b|),取值范围是 -1 到 1。用二维向量示意:
```text
@@ -845,6 +868,19 @@ a 和 b 几乎没有相同的字,相似度却很高,这正是向量检索相
用真实的模型计算,结论相同但数值没有这么极端:用 bge-small-zh-v1.5(512 维)编码这三句话,“怎么申请年假”与“休假的流程是什么”的余弦相似度为 0.603,与“今天股价上涨了”为 0.194(见[附录 A1](#a1-embedding-相似度与-bm25))。实际系统中不同句子的相似度大多落在一个不太宽的区间里,判断是否相关要看排名和阈值,而不是期望不相关的结果接近 0。
+意思相近的向量互相靠近,是用对比学习(Contrastive Learning)训练出来的。训练数据是成对的(问题,相关文档),一个批次中每个问题自己的文档是正样本,同一批次中其它问题的文档就是负样本,称为批内负样本(In-batch Negatives)。用 bge-small-zh-v1.5 编码三对数据,三段文档依次是“年假需提前 3 个工作日在 OA 系统提交申请”“错误码 E1024:库存不足”“北京、上海每晚住宿上限 600 元”,问题与文档的相似度如下:
+
+```text
+ 文档 1 文档 2 文档 3
+怎么申请年假 0.693 0.223 0.376
+错误码 E1024 是什么意思 0.318 0.762 0.266
+出差住宿标准是多少 0.380 0.339 0.627
+```
+
+训练时把每一行看成一个三选一的分类问题:相似度除以温度 τ 后做 softmax,损失是正确文档(对角线)的交叉熵,训练使对角线的值高于同一行的其它值。τ 用于放大差距,τ = 1 时第一行选中文档 1 的概率只有 0.43,τ = 0.05 时为 0.998(见[附录 A27](#a27-embedding-的池化与批内负样本))。
+
+负样本越多、越难区分,学到的向量越精细。所以 bge 训练时一个批次多达 19,200 对,后期还为每对数据挖掘一个难负样本(Hard Negative),即与问题字面相近但并不相关的文档,例如为 E1024 的问题配上 E1025 的说明。
+
#### 2. Top-K 检索
检索时把问题也转为向量,找出与它最相似的 K 个切片,称为 Top-K 检索。问题和切片必须使用同一个 Embedding 模型,更换模型后所有切片都要重新计算向量。K 怎么取见下文的 Top-K 与阈值。
@@ -936,9 +972,18 @@ FAISS 这类向量检索库只负责在内存中建立索引和检索。实际
- 逆文档频率:越少见的词权重越高,E1024 只出现在少数文档中,权重远高于“错误码”;
- 文档长度:词频相同时,较短文档的分数更高,避免长文档仅仅因为字数多而得分高。
-Elasticsearch 中 BM25 的参数 k1 和 b 默认为 1.2 和 0.75,分别控制词频的饱和速度和文档长度的影响程度。
+写成公式,t 遍历查询中的每个词:
-用同一个例子对比两种检索。查询“错误码 E1024 是什么意思”,候选文档是“错误码 E1024:库存不足”和“错误码 E1025:支付超时”:bge-small-zh 给出的相似度分别为 0.762 和 0.743,几乎分不开;BM25 中“错误码”出现在每篇文档里,逆文档频率很低,E1024 只出现在一篇文档中,权重高得多。在这两篇加一篇“常见错误码说明”组成的小语料上,按 k1 = 1.2、b = 0.75 计算,第一篇得 1.088 分,第二篇只有 0.130 分(见[附录 A1](#a1-embedding-相似度与-bm25))。
+```text
+score(q, d) = Σ IDF(t) × f × (k1 + 1) / (f + k1 × (1 − b + b × |d| / avgdl))
+IDF(t) = ln(1 + (N − n + 0.5) / (n + 0.5))
+```
+
+其中 f 是 t 在文档 d 中出现的次数,|d| 是文档的长度,avgdl 是所有文档的平均长度,N 是文档总数,n 是包含 t 的文档数。Elasticsearch 中 BM25 的参数 k1 和 b 默认为 1.2 和 0.75,分别控制词频的饱和速度和文档长度的影响程度。
+
+与经典的 TF-IDF 相比,BM25 改进了两处:TF-IDF 的得分与词频成正比,BM25 的词频部分不超过 k1 + 1,文档长度等于平均长度时,出现 1、5、10 次分别得 1.0、1.77、1.96;另外 BM25 按文档长度做了归一化。
+
+用同一个例子对比两种检索。查询“错误码 E1024 是什么意思”,候选文档是“错误码 E1024:库存不足”和“错误码 E1025:支付超时”:bge-small-zh 给出的相似度分别为 0.762 和 0.743,几乎分不开;BM25 中“错误码”出现在每篇文档里,逆文档频率很低,E1024 只出现在一篇文档中,权重高得多。在这两篇加一篇“常见错误码说明”组成的小语料上,按 k1 = 1.2、b = 0.75 计算,第一篇得 1.088 分,其中 E1024 贡献 0.958(IDF 为 0.981),“错误码”只贡献 0.130(IDF 为 0.134);第二篇只命中“错误码”,得 0.130 分(见[附录 A1](#a1-embedding-相似度与-bm25))。
因此通常两路同时召回,再合并结果。两路的分数不能直接相加,BM25 的分数没有上限,余弦相似度在 -1 到 1 之间。倒数排名融合(Reciprocal Rank Fusion, RRF)只看排名:文档的得分是它在各路结果中排名的倒数之和,即 Σ 1 / (k + rank),k 通常取 60。例如:
@@ -1357,6 +1402,8 @@ ReAct 适合下一步取决于上一步结果、步数不多的任务,例如
- 有可靠的外部反馈、允许多次尝试:反思;
- 步骤固定不变:不需要模型规划,直接写成工作流。
+需要注意的是,以上几种方式中,计划的内容都由模型生成,框架和代码只提供结构:ReAct 中下一步做什么由模型在每次调用时决定,代码只负责循环和限制步数;先规划后执行中,计划由模型写出,代码负责校验计划、按依赖调度和限制重规划的次数;思维树的搜索和回溯同样由代码实现。所以规划的质量主要取决于模型和提示词,框架决定的是执行过程是否可控。
+
### 记忆
模型本身不保存任何状态。模型服务的对话接口一般是无状态的,服务端不记录之前的请求,每次调用都要把需要的历史重新发过去。聊天应用看起来“记得”之前说过的话,是因为应用把这些信息保存了下来,在下一次调用时放回了上下文。
@@ -1611,6 +1658,8 @@ Skill 的核心是渐进式加载:
因此即使安装了几十个 Skill,平时占用的上下文也很少。
+判断是否使用某个 Skill 的是模型本身,而不是关键词匹配或者向量检索:所有 Skill 的名称和描述放在系统提示词中,模型认为当前任务与某个描述相符时,用读取文件的工具(例如用 cat 命令读取 SKILL.md)或者 Agent 提供的专门工具把正文读进上下文,再按正文中的步骤执行。
+
工具、MCP 和 Skill 的区别:
- 工具是一个可调用的函数,提供原子能力,例如查询订单;
@@ -1640,6 +1689,19 @@ description: 按团队模板生成周报。用户要求写周报、总结本周
name 只能包含小写字母、数字和连字符,最长 64 个字符;description 最长 1024 个字符,要同时写清楚做什么和什么时候用,Agent 就是根据它判断是否加载这个 Skill。需要接入外部系统、要求权限控制的能力(例如查询公司的工单系统)适合做成 MCP 服务端;告诉 Agent 怎样组合已有工具完成任务的流程和规范,适合写成 Skill。Skill 中可以包含脚本,等同于让 Agent 执行代码,只应该使用可信来源的 Skill。
+Skill 与系统提示词、Agent 的区别:
+
+- 系统提示词中的内容每次请求都占用上下文,适合每个任务都要遵守的少量规则,例如项目的构建命令;Skill 平时只占一行描述,适合只有某类任务才用到的流程和资料;
+- Agent 是执行任务的主体,由模型、循环、工具和上下文组成;Skill 不会自己运行,只是被 Agent 读进上下文的说明和附带的脚本。需要独立的上下文、并行执行或者单独的权限时,才拆成子 Agent,见[多 Agent](#多-agent)。
+
+写 Skill 的正文时,要记住它与对话历史共用上下文:
+
+- 只写模型不知道的内容,例如团队的模板、内部系统的用法,通用的知识不写。Anthropic 建议正文不超过 500 行,更长的内容拆到单独的文件中,由正文引用;
+- 写多细取决于任务能容忍多大的偏差:有多种合理做法的任务写目标和要点;必须严格一致的操作(例如数据库迁移)写成确定的命令或脚本,例如上面统计 PR 的部分交给 collect.py,而不是让模型自己数;
+- 质量要求高的任务在步骤中加入检查,例如“运行校验脚本,有错误就修改后重新运行,直到通过”。
+
+Skill 中的规范只能引导,不能保证模型一定遵守,必须满足的规则还要用脚本或钩子检查,见 [Harness 的引导与反馈](#2-引导与反馈)。
+
Agent 是否使用某个 Skill,完全取决于它的 description,所以要像测试[意图识别与路由](#意图识别与路由)一样测试它:
- 触发测试:准备约 20 条真实的提问,一半应该触发、一半不应该触发。不应该触发的重点选关键词相近但需求不同的,例如“把周报模板翻译成英文”不应该触发生成周报的 Skill。每条运行 3 次左右计算触发率,并留出一部分提问不参与修改,检查改进是否适用于新的说法;
@@ -1896,6 +1958,8 @@ String answer = ChatClient.create(chatModel)
框架根据方法签名生成参数的 JSON Schema,模型返回工具调用后,由框架执行方法、把结果发回模型,直到得到最终回答。工具抛出运行时异常时,默认把异常信息作为工具结果发回模型,让它修正或者向用户说明。需要注意的是,编译时要保留参数名(javac 的 -parameters 选项,继承 spring-boot-starter-parent 时默认开启),否则 Schema 中的参数名是 arg0(Spring AI 2.0.2 实测,见[附录 A9](#a9-spring-ai-的工具调用循环))。
+对话记忆由 ChatMemory 接口管理:默认的 MessageWindowChatMemory 保留最近的 20 条消息,系统消息始终保留;消息存放在 ChatMemoryRepository 中,默认在内存里,也可以换成 JDBC、Cassandra 等实现。使用时给 ChatClient 加上 MessageChatMemoryAdvisor,每次调用传入会话 ID,它在调用前按会话 ID 取出历史放进请求,调用后保存新的消息。需要注意的是,工具调用的请求和结果不会写入记忆,下一轮对话时模型看不到上一轮查过什么(Spring AI 2.0.2 实测,见[附录 A28](#a28-spring-ai-的对话记忆与工具调用)),需要时由应用自己保存,见[短期记忆](#1-短期记忆)。
+
Spring AI 主要解决模型接入和单个 Agent 的工具调用。Java 中另一个常用的框架是 LangChain4j,它不依赖 Spring,可以集成到 Spring Boot 或 Quarkus 中,特点是 AI Services:声明一个接口,框架生成实现这个接口的代理对象,与 Spring Data JPA 的 Repository 类似。两者的功能相近,Spring 项目用 Spring AI 与已有的配置、依赖注入结合得更自然。需要状态图编排时,可以用仿照 LangGraph 的 LangGraph4j,它可以与 LangChain4j 或 Spring AI 一起使用。
#### 4. 怎样选择
@@ -1907,6 +1971,13 @@ Dify、Coze 这类低代码平台用拖拽的方式编排工作流,内置模
通常的取舍是:验证想法、流程标准的内部工具用低代码平台;核心业务、对可靠性和集成要求高的系统用代码框架。两者也可以组合,例如平台中的流程通过接口调用自研的服务。
+LangChain 和 LangGraph 出自同一个团队,处在不同的层次:
+
+- LangChain 提供模型、工具、检索器等组件和现成的 Agent。1.0 版用 create_agent 创建标准的工具调用循环,摘要、按需选择工具、人工确认等逻辑以中间件(Middleware)的方式插入,早期的 Chain 等旧接口移到了 langchain-classic 包中;
+- LangGraph 是更底层的编排运行时,由开发者用状态图定义每个节点和分支,并提供持久化、流式输出和人工介入。create_agent 本身就运行在 LangGraph 之上。
+
+所以标准的“模型加工具”循环用 LangChain 几行就能写完;流程中既有固定的步骤又有模型的决策、需要精确控制每一步的走向时,例如上面的退款流程,用 LangGraph 自己定义图。
+
代码框架之间的选择可以参考:
- Java 项目优先考虑 Spring AI 或 LangChain4j,与现有的 Spring 体系集成;Python 项目常用 LangChain、LangGraph;
@@ -2357,6 +2428,14 @@ BM25 错误码 E1025:支付超时:0.130
BM25 常见错误码说明:0.140
```
+按词拆开第一篇的得分(文档长度按切出的词数计算,三篇分别为 6、6、5,平均 5.667):
+
+```text
+E1024:n = 1,IDF = 0.981,词频部分 = 0.977,贡献 0.958
+错误码:n = 3,IDF = 0.134,词频部分 = 0.977,贡献 0.130
+词频部分(文档长度等于平均长度):f = 1、2、5、10 时为 1.000、1.375、1.774、1.964
+```
+
### A2 提示词对工具调用的影响
模型用 float16 加载到 MPS,按“订单 1024 到哪了?”运行 Agent 循环,最多 5 步,采样参数 temperature = 0.7、top_p = 0.9,每次用不同的随机种子。最终回答中出现“杭州”(物流工具返回的当前位置)记为成功:
@@ -3133,6 +3212,76 @@ qwen2.5-3b 提示词 A 校验钩子 有: 成功 2/10;工具调用 [1, 1, 1, 1,
7B 在提示词 A 下没有校验时的典型回答是“您的订单已发货,运单号是 SF1234567。我可以帮您查询物流信息。”,即只查了订单就结束。
+### A27 Embedding 的池化与批内负样本
+
+bge-small-zh-v1.5(模型目录中 1_Pooling/config.json 的 pooling_mode_cls_token 为 true),transformers 在 Apple M1 Pro 的 CPU 上运行。分别用 [CLS] 位置的向量和所有 Token 的平均值作为句向量,归一化后计算点积;再编码三对(问题,文档),计算相似度矩阵,以及不同温度下以对角线为正样本的交叉熵:
+
+```python
+def enc(texts, pool="cls"):
+ x = tok(texts, padding=True, return_tensors="pt")
+ with torch.no_grad():
+ h = model(**x).last_hidden_state
+ if pool == "cls":
+ v = h[:, 0]
+ else:
+ mask = x["attention_mask"].unsqueeze(-1).float()
+ v = (h * mask).sum(1) / mask.sum(1)
+ return F.normalize(v, dim=-1)
+
+questions = ["怎么申请年假", "错误码 E1024 是什么意思", "出差住宿标准是多少"]
+docs = ["年假需提前 3 个工作日在 OA 系统提交申请", "错误码 E1024:库存不足", "北京、上海每晚住宿上限 600 元"]
+sim = enc(questions) @ enc(docs).T
+loss = F.cross_entropy(sim / tau, torch.arange(3), reduction="none")
+```
+
+```text
+tokens ['[CLS]', '怎', '么', '申', '请', '年', '假', '[SEP]']
+last_hidden_state (1, 8, 512)
+cls 年假-休假 0.603 年假-股价 0.194
+mean 年假-休假 0.547 年假-股价 0.189
+sim matrix
+ [[0.693 0.223 0.376]
+ [0.318 0.762 0.266]
+ [0.38 0.339 0.627]]
+tau 1.0 loss per row [0.856 0.811 0.929] mean 0.865 p_diag [0.425 0.444 0.395]
+tau 0.05 loss per row [0.002 0. 0.01 ] mean 0.004 p_diag [0.998 1. 0.99 ]
+```
+
+### A28 Spring AI 的对话记忆与工具调用
+
+Spring AI 2.0.2(spring-ai-client-chat),JDK 21。ChatClient 加上 MessageChatMemoryAdvisor,记忆使用默认参数的 MessageWindowChatMemory;桩模型第 1 次调用返回调用 getOrder 的请求,之后返回文本,并打印每次收到的消息。同一个会话先问“订单 1024 到哪了?”,再说“谢谢”:
+
+```java
+ChatMemory memory = MessageWindowChatMemory.builder().build();
+ChatClient client = ChatClient.builder(new StubModel())
+ .defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build()).build();
+client.prompt().user("订单 1024 到哪了?").tools(new OrderTools())
+ .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user-7")).call().content();
+client.prompt().user("谢谢")
+ .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user-7")).call().content();
+memory.get("user-7").forEach(m -> System.out.println(" " + m.getMessageType() + ": " + m.getText()));
+```
+
+```text
+--- 第 1 次调用模型,收到 1 条消息:
+ USER: 订单 1024 到哪了?
+--- 第 2 次调用模型,收到 3 条消息:
+ USER: 订单 1024 到哪了?
+ ASSISTANT(tool_calls):
+ TOOL: tool result
+--- 第 3 次调用模型,收到 3 条消息:
+ USER: 订单 1024 到哪了?
+ ASSISTANT: 订单 1024 已发货,运单号 SF1234567。
+ USER: 谢谢
+=== 记忆中 user-7 的消息:
+ USER: 订单 1024 到哪了?
+ ASSISTANT: 订单 1024 已发货,运单号 SF1234567。
+ USER: 谢谢
+ ASSISTANT: 好的。
+```
+
+同一轮之内,工具调用和结果在请求中;下一轮对话时,请求中只剩用户消息和最终回答。
+
## 参考资料
- [Attention Is All You Need](https://arxiv.org/abs/1706.03762)
@@ -3304,3 +3453,16 @@ qwen2.5-3b 提示词 A 校验钩子 有: 成功 2/10;工具调用 [1, 1, 1, 1,
- [Harness design for long-running application development](https://www.anthropic.com/engineering/harness-design-long-running-apps)
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
- [Pi](https://pi.dev/)
+- [C-Pack: Packed Resources For General Chinese Embeddings](https://arxiv.org/abs/2309.07597)
+- [Qwen3-Embedding-0.6B 模型说明](https://huggingface.co/Qwen/Qwen3-Embedding-0.6B)
+- [Parameter-Efficient Transfer Learning for NLP](https://arxiv.org/abs/1902.00751)
+- [Prefix-Tuning: Optimizing Continuous Prompts for Generation](https://arxiv.org/abs/2101.00190)
+- [The Power of Scale for Parameter-Efficient Prompt Tuning](https://arxiv.org/abs/2104.08691)
+- [GPT Understands, Too](https://arxiv.org/abs/2103.10385)
+- [DoRA: Weight-Decomposed Low-Rank Adaptation](https://arxiv.org/abs/2402.09353)
+- [PEFT: Soft prompts](https://huggingface.co/docs/peft/main/en/conceptual_guides/prompting)
+- [LoRA Without Regret](https://thinkingmachines.ai/blog/lora/)
+- [Agent Skills: Best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices)
+- [LangChain v1 Release Notes](https://docs.langchain.com/oss/python/releases/langchain-v1)
+- [LangChain Overview](https://docs.langchain.com/oss/python/langchain/overview)
+- [Spring AI: Chat Memory](https://docs.spring.io/spring-ai/reference/api/chat-memory.html)