Agent基础概念 - 从RAG到记忆
Agent 基础概念:从 RAG 到记忆
一篇过完六个基础问题:RAG 的召回与混合检索,RAG 的工程链路,Embedding 的数值和语义从哪来,Transformer 与 LLM 为什么无记忆,Agent 循环与 harness,Agent 记忆的 md 与数据库两条路。
只记机制和数字,能手算的手算一遍。
1. RAG:召回、索引、混合检索
RAG 是 Retrieval-Augmented Generation,检索增强生成:回答前先从外部知识库找出相关内容拼进 prompt,让模型照着材料答。
补的是两个短板:训练截止后知识过时,没材料时会编。
模型像一个只在入职培训时读过一遍文档的同事,RAG 是让他每次回答前先翻 Confluence,翻错页他会一本正经地按错页答,所以工程重心全在检索。
召回、重排、检索
我一开始把 retrieve 直接理解成”取”,工程上习惯叫召回,词是从搜索和推荐系统借来的。
- 召回(recall):粗筛。从几十万个 chunk 里快速捞出几十上百个候选,追求快和不漏。
- 重排(rerank):精排。对候选逐个精算相关性,只留前几名。
- 检索(retrieve):常和召回混用,严格说指召回加重排整条链。
标准流程是召回 top-50,重排取 top-5,拼进 prompt。
像 SQL 先 WHERE 走索引缩小范围,再 ORDER BY 在小结果集上排序,没人对全库 chunk 做重排。
两种索引,一份数据
文档切成 chunk 后建索引。
同一个 chunk 可以同时建两种索引,数据只有一份,和 MySQL 一张表同时建 B+ 树索引和全文索引一样。
- 倒排索引:词到 chunk 列表的映射,关键词检索走它,代表算法 BM25。
- 向量索引:每个 chunk 存一个 embedding 向量,语义检索走它,按向量距离找最近。
BM25
把 query 里每个词对文档的贡献加起来:
代码块收起展开
score(q, d) = Σ IDF(t) · tf(t,d) · (k1 + 1) / ( tf(t,d) + k1 · (1 - b + b · |d| / avgdl) )
t∈q- tf,词频:词在这篇文档出现几次,但有饱和,k1 控制饱和速度,常取 1.2 到 2.0。出现 10 次的得分远不到出现 5 次的两倍,防止堆词刷分。
- IDF,逆文档频率:log((N - n + 0.5) / (n + 0.5) + 1),N 是文档总数,n 是含该词的文档数。“的”几乎每篇都有,IDF 接近 0;只出现过两次的错误码 IDF 很高。
- 长度归一化:b 常取 0.75。长文档词多、命中概率高,按长度压一压。
本质是字面匹配,快、精确、每一分都能说清是哪个词贡献的。
代价是对同义词无能为力,“轿车”和”汽车”在它眼里是两个无关的串。
向量检索
embedding 模型把 query 和每个 chunk 各压成一个几百到几千维的向量,相似度用余弦:
代码块收起展开
cos(a, b) = (a · b) / (|a| · |b|)查询时算 query 向量和每个 chunk 向量的余弦,取最大的 k 个。
暴力算是 O(N · d),百万级以上用 HNSW 这类近似最近邻结构,牺牲一点召回率换几十倍速度。
失败模式互补
- 同义改写:搜”轿车”想找”汽车”的段落。BM25 查不到,向量能查到。
- 精确 token:错误码 ERR_0x8007、函数名、型号、人名。BM25 一击命中;向量容易被一堆”错误相关”的泛泛段落盖掉,embedding 模型对没见过的生僻串表示很差。
- 生僻词:库里只出现一两次的词 IDF 极高,BM25 给它大权重;embedding 模型的词表里可能根本没有它。
- 跨语言、口语化提问:向量好,BM25 差。
单用向量的典型事故是搜一个错误码,召回的全是”如何处理异常”的通用段落;单用 BM25 是换个说法就什么都搜不到。
两路各取 top-k 再融合,就是混合检索。
RRF 融合
两路分数量纲不同,BM25 可能是 12.7,余弦是 0.83,不能直接相加,所以按排名融合:
代码块收起展开
RRF(d) = Σ 1 / (k + rank_i(d)) k 常取 60
i文档 A 在 BM25 排第 1、向量排第 3,得分 1/61 + 1/63 = 0.0164 + 0.0159 = 0.0323。
文档 B 只在向量排第 1,BM25 没召回它,得分 1/61 = 0.0164,A 赢。
两路都命中的文档天然占优,不用调任何权重。
重排
召回的几十条交给 cross-encoder reranker,bge-reranker 或 Cohere Rerank,把 query 和候选拼成一个输入让模型直接打相关性分。
双塔 embedding 是两边独立编码再算距离,互相看不见;cross-encoder 让两者在注意力里充分交互,准得多。
代价是每一对都要过一遍模型,所以只对召回后的几十条做,这是整条链里提升最稳的一步。
2. RAG 工程链路
按链路顺序,每层记主流做法和它解决的失败。
切块与索引
- 结构切块:按标题、段落、代码块边界切,不固定 500 字一刀。标题路径作为元数据挂在 chunk 上,比如”JVM / 垃圾回收 / G1”。
- 父子索引:子块两三百 token 用来算相似度,命中后把它所属的父块或整节交给模型。检索要短才准,生成要长才全,靠两级粒度解决。
- 上下文化 chunk:Anthropic 2024 年的 Contextual Retrieval。入库前让 LLM 给每个 chunk 生成一句”它在全文里是什么”,拼在前面再做 embedding 和 BM25。
一段只写”该方法返回 null”的 chunk 单独看不知道是谁的方法,加上”这段来自 HashMap.get 的实现说明”才能被正确召回。
报告 top-20 召回失败率从 5.7% 降到 2.9%,再接 reranker 到 1.9%;成本靠 prompt caching 压到每百万文档 token 约 1 美元。 - 多表示索引:同一 chunk 额外生成摘要或它能回答的几个假设性问题,检索时匹配这些代理表示,正文照旧返回。
查询改写
用户的问法和文档的写法经常对不上,先改 query 再检索。
- 多查询:一个问题改写成三到五个变体,各自召回后合并去重。
- 查询分解:“Redis 和 MySQL 的事务隔离有什么区别”拆成两个各自检索,再合并回答。
- HyDE:先让模型编一段假答案,拿假答案做向量检索,“答案长什么样”比”问题长什么样”更接近文档的写法。
- Step-back:问”Java 17 的 record 能不能继承”,先检索”record 的定义与限制”,命中率比原句高。
召回
- 混合检索:BM25 加向量,RRF 融合,默认配置。
- 元数据过滤:时间、来源、文档类型先硬过滤再算相似度,别让去年的旧文档和今年的一起比。
- GraphRAG:微软方案。先用 LLM 从文档抽实体和关系建图,Leiden 算法切社区,每个社区生成摘要。
问”这批文档整体讲了什么”对所有社区摘要做 map-reduce,问具体问题从实体邻域取子图。
普通 RAG 只能答”某段写了什么”,它能答”跨几十篇综合起来是什么”,代价是建图的 LLM 调用量很大。
生成侧
- 引用约束:答案每句标注来源 chunk 编号,生成后校验编号是否真在本次召回集里,挡住编造引用。
- 长上下文兜底:文档总量不大时把全文塞进百万级窗口,配合 prompt caching,很多场景比检索准。工程答案是两个都要,RAG 从海量里选,长上下文在选出来之后多给一点。
Agentic RAG
把检索做成工具,agent 自己决定查不查、查几轮、换什么关键词。
Self-RAG 和 CRAG 的思路是检索完先判断结果够不够、相关不相关,不够就改写再查,或转去搜网页。
编码场景走另一条路,不建向量索引:知识库就是一堆 md 文件,检索器是 grep 和 find,PowerShell 里对应 Select-String 和 Get-ChildItem。
模型自己把问题拆成几个关键词各搜一次,搜到的片段自己看,无关的丢掉,保留的回喂给自己,再判断”够不够”决定再搜一轮还是带引用输出。
代码里全是精确 token,向量检索反而糊;索引会过期;上传代码有隐私问题。
Claude Code、Codex CLI、Aider 都是这个选择,Sourcegraph Cody 一度用了 embedding 后来又拆掉。
它牺牲语义匹配,换个说法就搜不到;换来零基础设施、精确 token 必中,以及全过程可读可复现。
评估
检索和生成分开测,否则挂了不知道挂在哪一步。
- 检索:recall@k,前 k 条里有没有正确 chunk;MRR,正确 chunk 平均排第几。
- 生成:faithfulness,每句是否被召回材料支撑;answer relevance,是否真的回应了问题。
RAGAS、TruLens 底层都是让 LLM 当裁判打分。
评测集要自己造,从真实文档抽 50 到 200 个问答对,没有它调参全靠感觉。
一条典型落地链:结构切块加上下文化入库,多查询改写,混合召回 top-50,reranker 取 top-5,带引用生成,RAGAS 回归。
每层独立可换,先做混合检索和 reranker,这两步收益最大。
3. Embedding:向量的语义从哪来
Embedding 把离散的东西,词、句子、文档、图片,变成一个向量,让”意思相近”变成”向量距离近”。
最笨的表示是 one-hot:词表 5 万个词,“猫”是第 1234 位为 1、其余全 0,任何两个词距离都一样,“猫”和”狗”不比”猫”和”电饭锅”更近。
embedding 换成 768 维这样的低维稠密向量,数值是训练学出来的,结果”猫”和”狗”挨得近,还能做 国王 - 男 + 女 ≈ 女王。
Java 的 hashCode 故意让相近输入得到无关输出,embedding 正相反,是一种局部敏感的哈希。
下面用三维玩具例子手算,真实模型 768 或 1536 维,原理一样。
第 1 步:查表
模型里有一张矩阵,行数是词表大小,列数是维度,每个 token 按 id 取一行,相当于 HashMap<Integer, float[]>。
代码块收起展开
我 [ 0.1, 0.3, -0.2]
喜欢 [ 0.4, 0.1, 0.5]
猫 [ 0.8, -0.2, 0.5]
狗 [ 0.7, -0.1, 0.6]
汽车 [-0.6, 0.7, 0.1]数字没有人定,第 1 维不代表”动物性”,一开始是随机数,靠训练调出来。
第 2 步:句子压成一个向量,算余弦
平均池化,所有 token 向量逐维取平均:
代码块收起展开
A = "我 喜欢 猫"
= ( (0.1+0.4+0.8)/3, (0.3+0.1-0.2)/3, (-0.2+0.5+0.5)/3 )
= [ 0.433, 0.067, 0.267 ]
B = "我 喜欢 狗" = [ 0.400, 0.100, 0.300 ]
C = "我 喜欢 汽车" = [-0.033, 0.367, 0.133 ]
cos(A, B) = (0.433·0.4 + 0.067·0.1 + 0.267·0.3) / (0.513 · 0.510)
= 0.260 / 0.262 ≈ 0.99
cos(A, C) = (0.433·(-0.033) + 0.067·0.367 + 0.267·0.133) / (0.513 · 0.392)
= 0.046 / 0.201 ≈ 0.23“喜欢猫”和”喜欢狗”0.99,和”喜欢汽车”0.23。
向量库做的就是存好所有 chunk 的向量,查询时算一遍 cos,取最大的几个。
第 3 步:数字怎么训出来
以 word2vec 为例,训练目标是用一个词预测它旁边的词。
语料里出现”喂 猫”,就要让 猫 和 喂 的向量点积变大,做一次梯度更新:
代码块收起展开
v猫 = [0.8, -0.2, 0.5] u喂 = [0.5, 0.0, 0.3]
点积 = 0.8·0.5 + 0 + 0.5·0.3 = 0.55
sigmoid(0.55) = 0.634,离目标 1 还差 0.366
v猫 += 学习率 0.1 × 0.366 × u喂 = [0.018, 0, 0.011]
v猫 -> [0.818, -0.2, 0.511] 朝 喂 的方向挪了一小步语料里也有”喂 狗”,狗 也朝 喂 挪,两个词被同一批上下文拉着走,几百万次后自然挨在一起。
“汽车”旁边出现的是”开、加油、停”,被拉向另一片区域。
相似度是共现沉淀出来的副产品,没人告诉过模型猫和狗都是动物。
背后是分布假说:一个词的意思就是它周围出现的词。
第 4 步:现代模型多了什么
BGE、text-embedding-3 这类模型在查表和池化之间插了一叠 Transformer 层,token 向量经过注意力混合上下文,“吃苹果”和”苹果手机”里的”苹果”得到不同向量,word2vec 做不到。
训练目标换成对比学习:给一个问题、一个正确段落、一堆无关段落,把正确对的余弦拉大、无关对的拉小。
和第 3 步是同一个拉扯机制,只是作用在句子级,“相似”的定义从共现换成人标注的相关。
语义来自训练目标,架构只提供表达能力。
word2vec 没有 Transformer 照样有语义;直接拿 BERT 的输出做句子相似度比 word2vec 取平均还差,因为 BERT 的目标是完形填空,从没被要求让相似句子靠近。
Sentence-BERT 拿同一个 BERT 换成对比学习目标再练一遍,检索效果才突然变好,架构一个字没改。
实际一行代码:
代码块收起展开
from sentence_transformers import SentenceTransformer
m = SentenceTransformer("BAAI/bge-small-zh-v1.5") # 中文 embedding 模型,512 维
v = m.encode("我喜欢猫") # 返回 512 个 float,对应第 2 步的结果4. Transformer 与 LLM 的无记忆
《Attention Is All You Need》是 2017 年 Google 的 Transformer 论文。
之前翻译和语言建模的主流是 RNN 和 LSTM:一个词一个词按顺序读,第 100 个词要等前 99 个算完,没法并行,GPU 吃不满;距离远的词信息要经过很多步传递,容易丢。
注意力当时已经有了,但只是挂在 RNN 上的辅助模块,这篇论文把 RNN 整个扔掉,只留注意力。
自注意力
每个词生成三个向量:Query(我在找什么)、Key(我是什么)、Value(我能提供什么)。
每个词拿自己的 Q 和所有词的 K 算相似度,softmax 成权重,按权重把所有词的 V 加权求和,得到自己的新表示:
代码块收起展开
Attention(Q, K, V) = softmax( Q · Kᵀ / √d ) · V除以 √d 防止维度大时点积过大、softmax 饱和成 one-hot。
任意两个词之间只隔一步,不管距离多远;整句所有词的计算是一次矩阵乘法,天然并行。
- 多头注意力:同一层并行跑多组 Q、K、V,每组学不同的关系,一组管语法依赖,一组管指代,最后拼起来。
- 位置编码:没了循环,模型不知道词序,“猫咬狗”和”狗咬猫”会得到同一组向量,所以给每个位置加正弦、余弦编码进向量。
- 残差、LayerNorm 和前馈层:每层等于注意力加一个逐位置的小 MLP,都带残差连接,方便堆深。
Encoder 和 Decoder 各 6 层:Encoder 双向看全句,Decoder 用 mask 挡住未来的词只看左边,再用 cross-attention 看 Encoder 的输出。
并行化让海量 GPU 和数据把模型堆大成为可能。
BERT 拿走 Encoder,GPT 拿走 Decoder,今天所有 LLM 都是那个 Decoder 堆几十层、参数放大几万倍。
为什么无记忆
模型是无状态函数:权重训练后固定,每次调用对输入 token 序列算一次前向传播,输出概率分布,算完什么也不留。
- 推理不改权重:聊一万轮,参数一个比特都没变。
- “记得上文”是假象:多轮对话是客户端每次把整段历史重新拼进 prompt 发过去,模型每次从头读。断开会话、超出上下文窗口,前面的内容对它就不存在。
- 参数里的知识不算记忆:训练数据压缩成了权重,是静态先验,不能按需写入,不能精确读出,不随使用更新。
和 Servlet 一样:单例、无状态,每个请求独立处理,会话状态靠 HttpSession 在容器里另外存。
LLM 的 HttpSession 就是外挂的上下文窗口、记忆文件和向量库。
上下文窗口的大小是每次请求能塞进 request body 的历史上限,注意力只在窗口内的 token 之间算,窗口外的东西模型压根看不到。
确定性函数加采样器
同一个问题两次答案不同,来源有两个。
- 输入不同:上下文、系统提示、记忆文件、工具结果,多一个空格输入序列就变了,输出分布跟着变。
- 采样随机:更主要的原因。模型输出的是下一个 token 的概率分布,选哪个由采样器决定:按概率掷骰子,temperature 控制分布的平坦程度,top-k 或 top-p 限定候选范围。同一分布掷两次选出不同的词,后面整条序列就分叉。
采样器在模型外面,是外层程序的一部分。
LLM 等于确定性函数 f(输入) 到分布,再加一个随机采样器;temperature 为 0 每次取最大概率,理论上同输入同输出。
实践里仍有细微差异:GPU 并行归约的浮点加法顺序不固定,不同 batch 大小走不同 kernel,MoE 模型的路由受同批其他请求影响,都是数值噪声。
三个词别混:参数是权重,训练后冻结、推理只读;上下文是这一次请求的输入序列;记忆是存在模型外面、用时拼进上下文的状态,是输入的一部分,从来不是参数。
5. Agent 循环与 Harness
Agent 是把 LLM 放进循环,让它自己决定下一步做什么,直到任务完成。
和问答的区别有三件事:能用工具,模型输出”调用 X 工具,参数 Y”,外层程序真的执行再把结果喂回;有循环,思考、行动、观察、再思考反复跑,模型自己判断何时做完;模型做决策,走哪条路、调什么工具、要不要重试都临场定。
代码块收起展开
while True:
reply = llm(history + tools) # 把历史和工具清单交给模型
if reply.is_final_answer: # 模型说做完了
break
result = execute(reply.tool_call) # 外层程序真的执行工具
history.append(reply, result) # 结果拼回历史,下一轮模型能看到Claude Code 和 Codex CLI 就是这个循环接上了文件系统、终端和搜索。
循环的来历
“感知、决策、行动”的循环是老概念。
控制论、机器人、强化学习几十年前就有 agent-environment loop,《人工智能:一种现代方法》里 agent 的定义就是它,只是”决策”那一格是规则、搜索或 RL 策略。
2022 年 Chain-of-Thought 证明让模型先写推理再写答案,正确率大涨。
ReAct 论文让模型按 Thought、Action、Observation 固定格式输出,外层程序解析 Action 去真的执行,结果作为 Observation 拼回去再继续,“决策”那一格换成了 LLM,循环成立。
2023 年 LangChain 把 ReAct 做成库,AutoGPT 放开循环让模型自己定目标跑到底,OpenAI 六月推出 function calling 把靠文本硬解析的 Action 变成结构化输出,循环才稳定可靠。
之后 SWE-agent、Devin、Claude Code、Codex 把循环接上文件系统和终端。
循环是旧的,新的是把人写的策略换成了能看懂观察结果的语言模型。
Harness
Agent 等于模型加 harness。
模型只负责”给定输入吐出下一步”,循环本身和其他所有事都在 harness 里。
- 循环与调度:何时调模型、解析 tool call、执行、拼回结果、判断何时停。
- 工具层:工具定义、执行、沙箱隔离、权限确认。MCP 是这一层的插件协议,让工具标准化接入。
- 上下文管理:系统提示怎么拼、历史太长怎么压缩摘要、记忆文件何时读写、RAG 何时召回。
- 鲁棒性:重试、超时、错误信息怎么回喂给模型、并行子 agent 怎么编排。
词借自 test harness,把被测对象夹住、给它输入、接住输出的装置,被夹住的是模型。
像 Tomcat 之于 Servlet:Servlet 只处理请求本身,容器负责连接、线程、生命周期和安全。
LangChain、Agent SDK 是造 harness 的零件,Claude Code、Codex CLI 是造好的成品。
同一个模型换 harness 表现差别很大,重试次数、记忆路由、上下文预算全是 harness 层的事,模型一个参数没变。
串行任务
“下一步依赖上一步结果”的任务叫串行任务,强调依赖关系时叫依赖链任务。
- Prompt chaining:Anthropic《Building effective agents》的分类,任务拆成固定步骤,每步 LLM 输出是下一步输入,对面是 parallelization。
- Pipeline:工程叫法,步骤和顺序都固定。
- Multi-hop:RAG 和问答场景,先查出 A 才能拿 A 去查 B,比如”某公司 CEO 的母校在哪”。
- DAG 的依赖边:任务多了就是有向无环图,按拓扑序执行,没依赖的分支并行。
和 CompletableFuture 对应:有依赖用 thenCompose 串起来,没依赖用 allOf 并行等。
评估
端到端评估把 agent 当黑盒,只看输入到最终输出这一头一尾,结果合格算过。
简单、和真实目标一致,但挂了不知道挂在哪一步:召回错了、工具调错了还是总结写歪了,黑盒看不出来。
所以配过程评估,也叫轨迹评估:拆开每一步看工具选对没有、参数对不对、有没有绕弯路、每次召回准不准。
端到端定”过没过”,过程评估定”哪里错”,再针对那一步调优。
时间花在哪
一次调用里 runtime 自己的工作约 1 秒,大部分在 idle 等网络和模型;LLM 推理约 10 秒,是大头。
瓶颈在模型,缩短响应只能从模型侧下手:流式输出让用户先看到东西、工具调用并行、prompt caching 少算重复前缀、简单步骤换小模型。
trade-off 是”想多久”和”等多久”:thinking 越长质量越高,延迟和费用线性涨,reasoning effort 档位调的就是这个旋钮。
runtime 九成时间 idle 说明它是 I/O 密集型,一台机器能并发挂很多会话,成本在模型那头。
6. Agent 记忆:md 文件与数据库
Claude Code 的三层
- 短期,上下文窗口:对话本身就是记忆,长了自动压缩成摘要,连同剩余内容进入下一个窗口。
- 指令记忆,CLAUDE.md 层级:全局 ~/.claude/CLAUDE.md,项目根 CLAUDE.md,不进 git 的 CLAUDE.local.md;子目录 CLAUDE.md 碰到那个目录的文件时才懒加载。支持 @文件 导入,对话里 # 开头直接追加一条。
- 自动记忆,memory 目录:每个项目一个目录,MEMORY.md 是索引,每次启动进上下文;每条记忆一个带 frontmatter 的 md,需要时再读。
读写用的就是普通 Read 和 Write 工具,没有隐藏的数据库或 embedding 步骤,模型看到的和打开文件看到的一模一样。
md 为什么成了共识
- 模型原生格式:md 直接进 prompt,不经过切 chunk、向量化、召回这条有损链路,不存在召回不到。
- 确定性加载:放在 CLAUDE.md 里的东西每次必定在上下文里,向量记忆做不到。
- 可见、可改、可审:agent 行为不对,打开文件就能看到它被告诉了什么,改一行就修好。
- 跟着仓库走:项目级 CLAUDE.md 进 git,团队共享、PR 里 review、有 diff 历史。
- 零基础设施:不用向量库,不用 embedding 模型。
- 层级等于作用域:全局、用户、项目、子目录,子目录懒加载相当于按位置做的粗糙检索。
Codex 的 AGENTS.md、Cursor 的 rules、Gemini CLI 的 GEMINI.md、Copilot 的 instructions 全是同一思路,2025 年 AGENTS.md 做成了跨厂商开放标准。
像 Spring 的 application.yml,配置跟着工程走、分 profile 覆盖,谁改了 git 都记着。
边界也清楚:每个字每轮都烧 token,文件一多就贵,要写路由规则控制何时读;没有语义检索,规模上去只能靠索引加人工分类;只擅长稳定的规则和背景,对”上周三试过什么失败了”这类情节记忆很弱。
所以 md 是指令型记忆的标准做法,情节型记忆还没有定论。
数据库做记忆的两类
对代码库做 RAG:Cursor 在本地把代码切块算 embedding,索引存云端向量库,@codebase 和 agent 检索走语义召回;Copilot 的 @workspace、Windsurf、Continue.dev 同一套。
反向趋势是 Claude Code、Codex、Aider 故意不建索引,理由见第 2 节的 grep 路线。
跨会话记忆存数据库,这块 DB 是主流:
- ChatGPT memory:对话中抽取事实存库,之后每次注入;“参考历史对话”是对历史聊天记录做 RAG。
- Cursor Memories:旁路模型盯着对话自动提炼记忆,存库,用户确认后生效。
- Mem0:向量加图数据库。对话结束后 LLM 抽取事实,去重合并入库,下次按相关性召回 top-k 注入。有 OpenMemory MCP,能插进 Claude Code 或 Cursor。
- Letta,原 MemGPT:核心记忆放 prompt 里让 agent 自己编辑,归档记忆放向量库,agent 用工具读写,“agent 自管记忆”的鼻祖。
- Zep、Graphiti:带时间维度的知识图谱,能答”这件事后来变了没有”。
稳定规则和项目背景用 md,海量情节记忆用数据库。
Claude Code 和 Codex 只做了前者,留了 MCP 口子,装个 Mem0 的 MCP server 就能接上后者。
.claude 目录与 md、json 的分工
代码块收起展开
~/.claude/ # 用户级,跨项目生效
├── CLAUDE.md # 全局指令
├── settings.json # env、permissions、hooks、model、statusLine
├── commands/ skills/ agents/ rules/ # 斜杠命令、技能、子代理、规则片段,全是 md
├── plugins/ # 插件市场与已装插件
├── projects/<cwd-slug>/ # 每个工作目录一份:<session>.jsonl 会话记录,memory/ 自动记忆
└── .credentials.json # 登录凭据
~/.claude.json # 全局状态:引导标记、各项目的信任、MCP 与统计
<repo>/ # 项目级,进 git 与团队共享
├── CLAUDE.md CLAUDE.local.md .mcp.json
└── .claude/ settings.json settings.local.json commands/ skills/ agents/ rules/同一套子目录名在用户级和项目级重复出现,作用域不同。
优先级从高到低:企业托管策略,命令行参数,项目 settings.local.json,项目 settings.json,用户级 settings.json,越靠近具体环境越优先。
我一开始猜 md 是给结构化、json 是给键值对,方向反了,分界线是谁来读。
md 给模型读:CLAUDE.md、SKILL.md、agents、commands、memory 原样拼进 prompt,故意低结构,人也能直接读、直接改、看 diff。
json 给程序读:settings、.mcp.json、~/.claude.json、会话 jsonl 模型从不看,是 harness 代码在读写。
权限按 schema 校验,多层 settings 合并,点一次”不再询问”程序自动改写 settings.local.json,这些需要严格 schema 和确定性序列化;会话记录用 jsonl 是要一行一个事件追加写。
真正结构化的是 json 那边,把 CLAUDE.md 做成 json 就是字符串里套字符串。
交界处是 SKILL.md 和 agents 头部的 YAML frontmatter,name、description、allowed-tools、model:程序只读这一小块决定要不要加载、何时加载,正文只在加载时才进上下文。
Codex 用 TOML 做配置,同样是程序读,只是能写注释,JSON 不能。
md 和 txt 喂给模型的是同样的字节,多的是一套可选的轻标记:训练语料里 README、文档、StackOverflow 全是 Markdown,标题分组和列表比一大段散文更容易被逐条遵守,命令放进代码围栏才不会被当成一句话;GitHub、Obsidian 会渲染;frontmatter、按标题定位、识别代码块都有现成工具。
不写 # 和 - 它就是纯文本,md 是 txt 的超集,模型自己的输出也是 Markdown。
延伸阅读
- RAG 与 Agent 工程
- AI 工程基础入门到精通
- Agent CLI 源码剖析 02:主循环与状态机
- Agent CLI 源码剖析 04:上下文工程与记忆系统
- Agent CLI 深水区 04:上下文预算、压缩与记忆
- Agent CLI 深水区 08:搜索系统与代码上下文
- Agent CLI 深水区 10:Trace、Eval 与调试体系
- Agent Harness 工程:从最小循环到生产运行时
- 01. MemGPT:把 LLM 当成操作系统来管理记忆
- 05. Mem0:生产级长期记忆框架
- 08. HippoRAG:受海马体启发的长期记忆检索
- 20. Reflexion:把失败后的反思写成语言记忆