[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-82104":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":13,"contributorsCount":13,"subscribersCount":13,"size":13,"stars1d":13,"stars7d":13,"stars30d":15,"stars90d":13,"forks30d":13,"starsTrendScore":13,"compositeScore":13,"rankGlobal":10,"rankLanguage":10,"license":10,"archived":16,"fork":16,"defaultBranch":17,"hasWiki":18,"hasPages":16,"topics":19,"createdAt":10,"pushedAt":10,"updatedAt":20,"readmeContent":21,"aiSummary":22,"trendingCount":13,"starSnapshotCount":13,"syncStatus":23,"lastSyncTime":24,"discoverSource":25},82104,"AgentLadder","etherea1ity\u002FAgentLadder","etherea1ity","Climb from LLM API to RL Agent with Klara","",null,"TypeScript",30,0,25,4,false,"main",true,[],"2026-06-12 02:04:23","# 章节二：RAG Agent，克拉拉的阳光图书馆\n> 从模型本身能力，到可检索、可引用、可追踪的本地知识系统\n\n当前分支：`v0.2-rag-agent`\n下一分支：`v0.3-agentic-rag`，会继续加入 query rewrite、retrieval planning、evidence selection、citation verification 和不足证据 fallback。\n\n## 如何运行\n\n```powershell\n# 1. 准备 API Key\n# 在 .env 中配置 DASHSCOPE_API_KEY=你的百炼 Key\n\n# 2. 构建本地 RAG 索引\npy scripts\u002Frag\u002Fbuild_index.py\n\n# 3. 启动后端和前端\npowershell -ExecutionPolicy Bypass -File .\\start.ps1 -NoOpen\n\n# 4. 打开前端\n# http:\u002F\u002F127.0.0.1:5123\n```\n\n可以用这些问题触发 RAG：\n\n```text\nWhat does this chapter teach?\nTell me about chapter 2.\nWhat did Klara learn in chapter one?\nWhat is AnswerFrameV1?\n```\n\n## 当前界面\n\n![Klara v0.2 Home](docs\u002Fscreenshots\u002Fv0.2-klara-home.png)\n\n![Klara v0.2 RAG Run Chain](docs\u002Fscreenshots\u002Fv0.2-rag-run-chain.png)\n\n## 本章总结\n在第一章中，Klara 只是一个能够调用 LLM、完成单次问答的 Minimal Agent。\n\n她能够听见用户的问题，也能够依靠模型本身给出回答。但这时的 Klara 还没有真正属于自己的资料世界。她不知道自己的成长经历，不知道项目路线，也无法在回答中说明“我是从哪里知道这件事的”。\n\n在第二章中，我们为 Klara 建造她的第一间小书房：\n\nKlara's Sun Library\n\n这间小书房里放的不是庞大的互联网，也不是复杂的论文库，而是 Klara 最初应该读懂的资料：\n\n1. Klara 的身份与设定\n2. Agent Ladder 的项目经历与成长路线\n\n从这一章开始，Klara 不再只依靠模型参数来回答问题，她会学习**如何读取本地文档**、**切分知识片段**、**生成 embedding**、**建立向量索引**、**召回相关资料**、**对资料进行粗\u002F细粒度重排序**，并把 evidence、SourceCard、Citation 和 AnswerFrameV1 串成一条可观察链路。\n并且还有一些辅助克拉拉理解知识的设计，比如 Metadata、Index、意图识别、Context Builder 等。\n\n在每一次问题中，克拉拉都会告诉我们，她每一步都分别做了什么，我们可以从一个完整的回答链路中，看到每一次公开运行链路的详细过程。\n\n## Part A：本章定位\n### 1. 为什么需要RAG\n在第一章中，Klara已经能完成一次最小问答。当我们提出一个问题，她可以接受输入，调用LLM，生成回答，并且记录这一次的trace。\n但是，她只能依靠LLM自己的能力去做出反应。如果你要问Klara，你的这个Klara：Agent Ladder是在做什么，我们的第一章学到了什么，第二章有什么能力，她并不知道。因为这些问题不属于普通常识，而是属于我们的私有知识、阶段知识以及需要持续更新的知识。\nRAG 的价值就在这里：它可以给 LLM 外接一个可维护的知识库，让知识不再只依赖模型本身，而是可以被新增、修改、删除、重新索引和重新检索。这样 Klara 的回答就能随着项目一起更新，也能在回答时说明自己参考了哪些资料。\n并且当我们的知识有了源头，Klara就可以对自己的回答进行溯源，避免很多幻觉问题。\n\n一次基础 RAG 流程可以理解为：Markdown + Metadata → Document → TextChunk → IndexRecord → Embedding → Local Vector Index + BM25 → Hybrid Retrieval → Reranking → Context Builder → Klara Writer → AnswerFrameV1 → Run Chain \u002F Trace。也就是说，我们先把资料清洗、切块、加上 metadata，再将文本向量化并存入索引；当用户提问时，系统会通过关键词和向量等方式进行混合检索，先粗排召回可能相关的内容，再精排选出最重要的片段，最后把这些片段交给 LLM 生成有来源支撑的回答。\n\n### 2. Klara's Sun Library：为什么是小书房\n第二章中，我们不会直接让 Klara 读取整个互联网，也不会一上来放入大量复杂论文。我们先给她一间小而清晰的书房：Klara's Sun Library。这间小书房里放的是 Klara 最应该先读懂的内容：她是谁、她的性格和边界是什么。Agent Ladder 的项目路线是什么、第一章发生了什么、第二章要学习什么。\n\n之所以叫小书房，是因为第二章的重点不是堆资料规模，而是讲清楚 RAG 从前到后的基础链路。Klara 不需要一开始就拥有一座庞大的图书馆，她需要先学会如何整理资料、如何检索资料、如何选择相关片段、如何把资料交给 LLM，以及如何展示来源。从这一章开始，Klara 不再只是回答，而是开始学会先查资料后再回答。\n\n因此，本章的目标可以概括为：让 Klara 拥有第一批可检索、可更新、可引用、可追踪的本地知识。第二章的 Klara 学会的是基础 RAG：如何把小书房里的资料变成可检索知识，并基于这些知识生成有来源的回答。第三章才会进一步升级到 Agentic RAG，让 Klara 学会复杂意图拆分、检索规划、query rewrite、证据选择和回答验证。\n## Part B：资料进入系统\n\nPart B 只做两件事：先读取 Klara 小书房里的 markdown 和 metadata，再把标准化后的 `Document` 切成 `TextChunk[]`。\n\n```text\nMarkdown + Metadata\n→ Document\n→ TextChunk[]\n```\n\n这一部分还不涉及 embedding，也不涉及检索算法。它的目标是让磁盘上的知识文件进入系统，并变成下一部分索引层可以继续处理的文本块。\n\n### 3. 读取与 Metadata 设计\n\n这一节解决的问题是：Klara 如何把本地知识文件带着身份信息读入系统。\n\n```text\n.md file + .metadata.yaml file\n→ LocalMarkdownLoader\n→ Document\n```\n\n输入是小书房里的 markdown 正文和同名 metadata 文件；输出是统一的 `Document`。Klara 在这里学会：每一份资料都不只是文本，还必须知道它来自哪里、属于哪一章、对应哪个版本。\n\n对应代码：\n\n```text\nsrc\u002Fagent_ladder\u002Frag\u002Fcontracts\u002Fdocument.py\nsrc\u002Fagent_ladder\u002Frag\u002Fingestion\u002Flocal_markdown.py\n```\n\n\u003Cdetails>\n\u003Csummary>展开：资料、metadata 字段与 Document 设计\u003C\u002Fsummary>\n\nKlara 的小书房里现在有三份最初的英文知识资料：\n\n```text\ndata\u002Fknowledge\u002F\n├── global\u002F\n│   ├── klara-overview.md\n│   └── klara-overview.metadata.yaml\n└── chapters\u002F\n    ├── ch01-minimal-agent.md\n    ├── ch01-minimal-agent.metadata.yaml\n    ├── ch02-rag-agent.md\n    └── ch02-rag-agent.metadata.yaml\n```\n\n其中：\n\n- `klara-overview.md`：Klara 的全局能力地图，说明 Klara 是谁、Agent Ladder 是什么、她会沿着哪些能力成长\n- `ch01-minimal-agent.md`：Klara 第一章已经学会的 Minimal Agent 能力\n- `ch02-rag-agent.md`：Klara 第二章正在学习的 RAG Agent 能力\n\n每一份 markdown 都有一个同名 metadata 文件。例如：\n\n```text\nch02-rag-agent.md\nch02-rag-agent.metadata.yaml\n```\n\n这样设计是因为正文和身份信息要分开：\n\n- `.md` 负责保存 Klara 能阅读的知识正文\n- `.metadata.yaml` 负责说明这份资料是谁、来自哪里、属于哪一章、对应哪个版本\n\n读取后的结果是统一的 `Document`：\n\n```text\nMarkdown File\n+\nMetadata File\n→ Document\n```\n\n在代码里，`Document` 的最小结构是：\n\n```text\nDocument = text + metadata\n```\n\n其中 metadata 包含：\n\n```text\ndocument_id\ntitle\nsource_path\nsource_type\ncategory\nchapter\nversion\nlanguage\ntags\nsummary\n```\n\n例如 `ch02-rag-agent.metadata.yaml` 会告诉系统：\n\n```text\ndocument_id: doc_ch02_rag_agent\ntitle: Chapter 2 Capability: RAG Agent\ncategory: chapters\nchapter: ch02\nversion: v0.2-rag-agent\nsource_type: markdown\n```\n\n这一层解决的是：Klara 的资料如何带着身份进入系统。后续做 source card、citation、版本过滤、章节过滤时，都依赖这些 metadata。\n\n\u003C\u002Fdetails>\n\n### 4. 分块策略：Overlap Chunking\n\n这一节解决的问题是：整篇 `Document` 太长，不适合直接检索，需要切成更小的文本块。\n\n```text\nDocument\n→ OverlapTextSplitter\n→ TextChunk[]\n```\n\n输入是 `Document`，输出是带来源信息的 `TextChunk[]`。Klara 在这里学会：检索系统通常检索的是片段，不是整篇文章；相邻片段保留一点 overlap，可以减少边界信息丢失。\n\n对应代码：\n\n```text\nsrc\u002Fagent_ladder\u002Frag\u002Fcontracts\u002Fchunk.py\nsrc\u002Fagent_ladder\u002Frag\u002Fchunking\u002Foverlap.py\n```\n\n\u003Cdetails>\n\u003Csummary>展开：常见切块策略与本章为什么选择 overlap\u003C\u002Fsummary>\n\n如果整篇文档太长，检索会变粗；如果片段太短，又容易丢失上下文。所以 RAG 系统通常会使用 chunking 策略。\n\n常见的 chunking 策略包括：\n\n```text\nfixed-size chunking\nrecursive markdown chunking\nheading-based chunking\nsemantic chunking\noverlap chunking\n```\n\n这一章先不做复杂策略，只选择一个最基础、最容易理解、也最常见的策略：\n\n```text\noverlap chunking\n```\n\n它的意思是：相邻 chunk 之间保留一小段重叠文本。\n\n例如：\n\n```text\nchunk_size = 800\nchunk_overlap = 120\n```\n\n这样切块时，大概会形成：\n\n```text\nChunk 1: characters 0-800\nChunk 2: characters 680-1480\nChunk 3: characters 1360-2160\n```\n\n重叠部分可以减少边界信息丢失。\n\nPart B 结束时，Klara 的资料会从：\n\n```text\nMarkdown + Metadata\n```\n\n变成：\n\n```text\nDocument\n→ TextChunk[]\n```\n\n这些 chunk 还没有向量，也还不能被检索。它们只是进入下一部分算法层的输入。\n\n\u003C\u002Fdetails>\n\n## Part C：从文本块到可检索索引\n\nPart C 的目标是：让每一个 `TextChunk` 进入索引层，拥有语义表示和关键词表示，最终可以被检索、融合排序和精排。\n\n```text\nTextChunk[]\n→ IndexRecord[]\n→ Dense Embedding\n→ Dense Vector Index\n→ Sparse \u002F BM25 Index\n→ Hybrid Retrieval\n→ Reranked Results\n```\n\n### 5. IndexRecord：Chunk 如何进入索引层\n\n这一节解决的问题是：`TextChunk` 只是文档切块层对象，不能承担 embedding、tokens、scores 等检索状态。\n\n```text\nTextChunk[]\n→ records_from_chunks()\n→ IndexRecord[]\n```\n\n输入是 `TextChunk[]`，输出是 `IndexRecord[]`。Klara 在这里学会：把“文档切块层”和“索引检索层”分开，后面的 dense vector、sparse tokens、BM25 信息和检索分数都放到索引层对象里。\n\n对应代码：\n\n```text\nsrc\u002Fagent_ladder\u002Frag\u002Findexing\u002Findex_record.py\n```\n\n\u003Cdetails>\n\u003Csummary>展开：为什么需要 IndexRecord，以及它和真实向量库的关系\u003C\u002Fsummary>\n\nPart B 的终点是 `TextChunk`。一个 `TextChunk` 只说明：\n\n```text\n这段文本来自哪份 Document\n它是第几个 chunk\n它在原文中的起止位置是什么\n它带着哪些 metadata\n```\n\n例如：\n\n```text\nchunk_id: doc_ch02_rag_agent_chunk_0003\ndocument_id: doc_ch02_rag_agent\ntext: \"RAG lets Klara search local knowledge before answering...\"\nmetadata:\n  chapter: ch02\n  version: v0.2-rag-agent\n```\n\n但是检索系统需要的不只是文本。Dense retrieval 需要：\n\n```text\ndense_vector\n```\n\nSparse \u002F BM25 retrieval 需要：\n\n```text\nsparse_tokens\nterm frequency\ndocument length\n```\n\nHybrid retrieval 后面还会产生：\n\n```text\ndense_score\nsparse_score\nhybrid_score\n```\n\n这些都不应该直接塞回 `TextChunk`。因为 `TextChunk` 的职责是表示文本如何从 `Document` 中切出来，而索引层需要一个新的对象：\n\n```text\nIndexRecord = TextChunk + 检索层信息\n```\n\n在本章的最小版本里，`IndexRecord` 包含：\n\n```text\nrecord_id\nchunk_id\ndocument_id\ntext\nmetadata\ndense_vector\nsparse_tokens\ntoken_count\n```\n\n这一层的意义是把三个世界分开：\n\n```text\nTextChunk      = 文档切块层\nIndexRecord    = 索引检索层\nRetrievedChunk = 检索结果层\n```\n\n真实向量库里也有类似边界：\n\n```text\nQdrant point      = id + vector + payload\nWeaviate object   = properties + vector + inverted index entry\nElasticsearch doc = _source + dense_vector field + text field\n```\n\n我们现在不直接接这些数据库，而是先手写一个教学版 `IndexRecord`。这样后面无论换成本地 JSONL、Qdrant、Weaviate、Milvus，核心结构都不会乱。\n\n\u003C\u002Fdetails>\n\n### 6. Dense Embedding：把文本变成语义向量\n\n这一节解决的问题是：普通文本不能直接做语义相似度计算，需要先变成 dense vector。\n\n```text\nIndexRecord.text\n→ Embedding Model\n→ dense_vector\n```\n\n输入是 `IndexRecord.text`，输出是 `dense_vector`。Klara 在这里学会：把 chunk 和用户 query 都转成向量，下一节再用相似度搜索找到相关资料。本章不训练 embedding model，而是调用已有 embedding model；Sparse \u002F BM25 部分会自己手写，Dense Embedding 使用真实模型生成语义向量。\n\n对应代码：\n\n```text\nsrc\u002Fagent_ladder\u002Frag\u002Fembeddings\u002Fbase.py\nsrc\u002Fagent_ladder\u002Frag\u002Fembeddings\u002Fdashscope.py\n```\n\n\u003Cdetails>\n\u003Csummary>展开：从 one-hot、Bag of Words 到 Dense Embedding\u003C\u002Fsummary>\n\n#### 为什么文本不能直接计算\n\n用户可能会问：\n\n```text\nWhat did Klara learn in chapter one?\n```\n\n资料里可能写的是：\n\n```text\nChapter 1 introduced AskState, AnswerState, RunLog, and the MinimalAgent runtime.\n```\n\n人可以看出它们相关，但是程序看到的只是两段字符串。字符串本身只能直接做一些很浅的比较：是否完全相等、是否包含某个词、两个字符串编辑距离是多少。\n\n它不知道：\n\n```text\nlearn ≈ introduced\nchapter one ≈ Chapter 1\nKlara's first ability ≈ Minimal Agent\n```\n\n所以，我们需要把文本变成数字。只有变成数字后，系统才能计算“这两个文本有多相似”。\n\n#### Vocabulary：先把世界变成词表\n\n最基础的方法是先定义一个词表：\n\n```text\nvocabulary = [\"klara\", \"agent\", \"rag\", \"answer\", \"trace\"]\n```\n\n词表里的每个词对应一个位置：\n\n```text\nklara  → 0\nagent  → 1\nrag    → 2\nanswer → 3\ntrace  → 4\n```\n\n这不是现代 embedding，但它能帮助我们理解：文本向量化的第一步，是把文本放进一个可计算的坐标系统。\n\n#### One-hot Encoding：一个词一个位置\n\n如果当前词是 `rag`，那么它在上面词表里的 one-hot 表示就是：\n\n```text\n[0, 0, 1, 0, 0]\n```\n\n如果当前词是 `klara`，就是：\n\n```text\n[1, 0, 0, 0, 0]\n```\n\none-hot 的特点是只有一个位置是 1，其他位置都是 0。它非常容易理解，但它只能表示“这是哪个词”，不能表示“这个词和哪个词语义更近”。\n\n#### Bag of Words：一句话里出现了哪些词\n\n一句话可以看成多个词的集合。例如：\n\n```text\nKlara uses RAG to answer.\n```\n\n在词表：\n\n```text\n[\"klara\", \"agent\", \"rag\", \"answer\", \"trace\"]\n```\n\n里，可以表示成：\n\n```text\n[1, 0, 1, 1, 0]\n```\n\n如果记录出现次数：\n\n```text\nKlara uses RAG. Klara answers.\n→ [2, 0, 1, 1, 0]\n```\n\n这就是 bag of words 的直觉。它比单个 one-hot 更像“文本向量”，但它仍然主要关心词有没有出现、出现了几次，还不真正理解语义。\n\n#### Sparse Vector 的问题\n\n词表向量通常是 sparse vector。真实词表可能有几万、几十万词，而一句话只会出现其中很少一部分，所以向量大概会长这样：\n\n```text\n[0, 0, 0, 1, 0, 0, 0, 0, ...]\n```\n\n这类表示的问题是：维度很大、大部分位置为空、只知道词出现没出现、不理解同义词、不理解改写后的相同含义。\n\n但是 sparse representation 也不是没用。它非常适合处理：\n\n```text\nAskState\nRunLog\nAnswerFrameV1\nv0.2-rag-agent\nsource_card\n```\n\n这些项目术语、字段名、版本号、代码名，往往需要精确匹配。所以后面第 8 节我们还会学习 BM25。\n\n#### Dense Embedding：把语义压缩进向量\n\n现代 embedding 模型做的是：\n\n```text\ntext → dense vector\n```\n\n例如：\n\n```text\n\"What did Klara learn in chapter one?\"\n→ [0.031, -0.482, 0.105, 0.774, ...]\n```\n\ndense vector 和 sparse vector 不同。它通常大部分位置都有小数值，每个维度不再对应一个人工指定的词，而是模型从大量数据里学出来的语义特征。\n\n所以这两句话虽然字面不同：\n\n```text\nWhat did Klara learn in chapter one?\nWhich abilities did Klara gain in the first chapter?\n```\n\n它们的 dense vector 仍然可能很接近。这就是 dense embedding 对 RAG 的价值：Klara 不只按字面找资料，也能按语义找资料。\n\n#### Cosine Similarity：比较两个向量方向\n\n当 chunk 和 query 都变成向量后，我们还需要一个相似度算法。最常用的是 cosine similarity：\n\n```text\ncosine_similarity(a, b)\n= (a · b) \u002F (||a|| × ||b||)\n```\n\n其中：\n\n```text\na · b = dot product，两个向量对应位置相乘再相加\n||a|| = 向量 a 的长度\n||b|| = 向量 b 的长度\n```\n\n直觉是比较两个向量的方向是否接近。如果两个向量方向越接近，分数越高。在 RAG 里就是：\n\n```text\nquery_vector\nvs\nchunk_vector\n```\n\n谁更接近，谁就更可能是相关资料。这会成为下一节 `Dense Vector Index` 的数学基础。\n\n#### 我们自己写 embedding model 吗？\n\n这一章不训练 dense embedding model。原因是 dense embedding model 本身需要大量语料、训练目标、对比学习数据、GPU、评估集和持续调优，这不是本章重点。\n\n本章采用：\n\n```text\nSparse \u002F BM25：我们自己手写\nDense Embedding：调用已有 embedding model\n```\n\n这样 Klara 可以使用真实语义向量，学习者仍然能手写并理解检索、BM25、hybrid 和 reranking。\n\n#### Embedding 存在哪里？\n\n刚生成的 embedding 可以先放在内存里的 `IndexRecord.dense_vector`：\n\n```text\nIndexRecord.text\n→ embedding model\n→ IndexRecord.dense_vector\n```\n\n但是如果每次运行都重新调用 embedding API，会慢，也会增加成本。所以后面第 7 节会把带向量的索引记录保存到本地：\n\n```text\ndata\u002Frag\u002Findex\u002Findex_records.jsonl\n```\n\n这一章先使用本地 JSONL。未来可以替换成真正的向量库：Qdrant、Weaviate、Milvus、Elasticsearch \u002F OpenSearch。\n\n#### Klara 这一章怎么做\n\nKlara 当前会使用 DashScope 的 OpenAI-compatible embedding API。这里的重点是区分：\n\n```text\nChat Model ≠ Embedding Model\n```\n\nChat model 负责生成回答：\n\n```text\nquestion → answer\n```\n\nEmbedding model 负责生成向量：\n\n```text\ntext → vector\n```\n\nKlara 会把每个 `IndexRecord.text` 送给 embedding model：\n\n```text\nIndexRecord.text\n→ text-embedding-v4\n→ dense_vector\n```\n\n这一节只负责 `Text → Dense Vector`。下一节才会继续 `Dense Vector → Vector Index → Similarity Search`。\n\n\u003C\u002Fdetails>\n\n### 7. Dense Vector Index：最小世界算法\n\n这一节解决的问题是：Klara 已经能把文本变成 `dense_vector`，但还需要把带向量的记录保存下来，并能根据用户问题的向量找出最相似的资料片段。\n\n```text\nIndexRecord[] with dense_vector\n→ LocalIndexStore\n→ SimpleVectorIndex\n→ DenseSearchResult[]\n```\n\n输入是带 `dense_vector` 的 `IndexRecord[]` 和用户问题的 `query_vector`，输出是按相似度排序的 `DenseSearchResult[]`。Klara 在这里学会：用本地 JSONL 保存索引记录，并用最小世界算法完成一次可解释的语义搜索。\n\n对应代码：\n\n```text\nsrc\u002Fagent_ladder\u002Frag\u002Findexing\u002Flocal_index_store.py\nsrc\u002Fagent_ladder\u002Frag\u002Findexing\u002Fsimilarity.py\nsrc\u002Fagent_ladder\u002Frag\u002Findexing\u002Fsimple_vector_index.py\n```\n\n\u003Cdetails>\n\u003Csummary>展开：JSONL 存储、cosine similarity、最小世界算法与 FAISS\u003C\u002Fsummary>\n\n#### 为什么要保存 embedding\n\n如果每次运行都重新做：\n\n```text\n读取 markdown\n→ 切 chunk\n→ 转 IndexRecord\n→ 调 embedding API\n→ 搜索\n```\n\n系统会变慢，也会重复消耗 API 调用。所以 embedding 生成后需要保存。\n\n本章使用最透明的本地 JSONL：\n\n```text\ndata\u002Frag\u002Findex\u002Findex_records.jsonl\n```\n\n每一行是一条 `IndexRecord`：\n\n```json\n{\n  \"record_id\": \"idx_doc_ch02_rag_agent_chunk_0003\",\n  \"chunk_id\": \"doc_ch02_rag_agent_chunk_0003\",\n  \"document_id\": \"doc_ch02_rag_agent\",\n  \"text\": \"RAG lets Klara search local knowledge before answering...\",\n  \"metadata\": {\n    \"source_path\": \"data\u002Fknowledge\u002Fchapters\u002Fch02-rag-agent.md\",\n    \"source_type\": \"markdown\",\n    \"category\": \"chapters\",\n    \"title\": \"Chapter 2 Capability: RAG Agent\",\n    \"chapter\": \"ch02\",\n    \"version\": \"v0.2-rag-agent\",\n    \"language\": \"en\",\n    \"tags\": [\"rag\", \"local-knowledge\"],\n    \"summary\": \"...\"\n  },\n  \"dense_vector\": [0.031, -0.482, 0.105],\n  \"sparse_tokens\": [],\n  \"token_count\": 0,\n  \"created_at\": \"...\"\n}\n```\n\nJSONL 的优点是：一行一条记录，人可以直接打开看，不需要数据库，也方便后面迁移到真正的向量库。\n\n#### Dot Product\n\n两个向量的 dot product 是对应位置相乘再相加。\n\n例如：\n\n```text\na = [1, 2, 3]\nb = [2, 1, 3]\n\ndot(a, b)\n= 1×2 + 2×1 + 3×3\n= 13\n```\n\n它衡量两个向量在方向上的一致程度，但会受到向量长度影响。\n\n#### Vector Norm\n\n向量长度，也叫 Euclidean norm：\n\n```text\n||a|| = sqrt(1² + 2² + 3²) = sqrt(14)\n||b|| = sqrt(2² + 1² + 3²) = sqrt(14)\n```\n\n#### Cosine Similarity\n\ncosine similarity 会把 dot product 除以两个向量长度：\n\n```text\ncosine_similarity(a, b)\n= dot(a, b) \u002F (||a|| × ||b||)\n```\n\n代入上面的例子：\n\n```text\ncos(a, b)\n= 13 \u002F (sqrt(14) × sqrt(14))\n= 13 \u002F 14\n≈ 0.928\n```\n\n它比较的是方向，而不是绝对长度。对 embedding 来说，这很适合，因为我们关心的是文本语义方向是否接近。\n\n#### 最小世界算法\n\n本章知识库很小，所以先使用最直观的 brute-force search：\n\n```text\nscores = []\n\nfor record in records:\n    score = cosine_similarity(query_vector, record.dense_vector)\n    scores.append((record, score))\n\nsort scores by score desc\nreturn top_k\n```\n\n这就是这里说的“最小世界算法”：遍历所有 record，一个一个算相似度，再排序取前几个。\n\n它不是最快的，但它最透明。学习者可以看到每一步：\n\n```text\nquery_vector 和每个 chunk_vector 比较\n→ 得到 score\n→ 排序\n→ top_k\n```\n\n#### FAISS 是什么\n\nFAISS 是 Meta 开源的相似向量搜索库。它解决的问题是：当向量数量从几十条变成几十万、几百万时，不能每次都全量遍历。\n\nFAISS 会使用向量索引结构来加速 nearest neighbor search。常见思路包括：\n\n```text\n把向量组织成索引\n减少需要比较的候选数量\n用近似最近邻搜索换取速度\n```\n\n所以 FAISS 的价值不是改变“向量相似度检索”的本质，而是在规模变大时更快地找到相似向量。\n\n本章不用 FAISS，是因为 Klara 的小书房只有少量资料，手写最小算法更适合教学。\n\n\u003C\u002Fdetails>\n\n### 8. Sparse \u002F BM25 Index：关键词检索\n\n这一节解决的问题是：dense vector 能找语义相近的内容，但对代码名、字段名、版本号、章节名这类精确词不一定稳定。\n\n```text\nIndexRecord[]\n→ Tokenizer\n→ BM25Retriever\n→ BM25SearchResult[]\n```\n\n输入是 `IndexRecord[]` 和原始 query 文本，输出是按关键词相关性排序的 `BM25SearchResult[]`。Klara 在这里学会：除了理解语义，也要能抓住 `AskState`、`RunLog`、`AnswerFrameV1`、`v0.2-rag-agent` 这样的精确项目术语。\n\n对应代码：\n\n```text\nsrc\u002Fagent_ladder\u002Frag\u002Fretrieval\u002Ftokenizer.py\nsrc\u002Fagent_ladder\u002Frag\u002Fretrieval\u002Fbm25.py\n```\n\n\u003Cdetails>\n\u003Csummary>展开：Sparse Retrieval、Inverted Index 与 BM25\u003C\u002Fsummary>\n\n#### 为什么 dense retrieval 不够\n\n如果用户问：\n\n```text\nWhat is AnswerFrameV1?\n```\n\n或者：\n\n```text\nWhere is RunLog created?\n```\n\n这些问题里有很强的项目术语。Dense embedding 可以理解语义，但未必总能稳定抓住这些精确符号。BM25 这类 sparse retrieval 更擅长关键词、字段名、版本号和代码名。\n\n#### Sparse Retrieval 是什么\n\nSparse retrieval 的核心是：把文本拆成 token，然后用词项出现情况检索。\n\n```text\nquery = \"AskState RunLog\"\nrecord text = \"Chapter 1 introduced AskState, AnswerState, and RunLog.\"\n```\n\n如果 query 里的词在 record 中出现，record 就应该得分更高。\n\n#### Inverted Index\n\n关键词检索常用 inverted index。它不是从文档找词，而是从词找文档：\n\n```text\naskstate → [record_001, record_004]\nrunlog   → [record_001, record_009]\nrag      → [record_010, record_011, record_012]\n```\n\n这样查询 `AskState` 时，不需要遍历所有文本，就能直接找到包含这个词的记录。本章为了教学会先手写最小 BM25，数据量小的时候也可以直接扫描 records。\n\n#### TF：Term Frequency\n\nTF 表示一个词在当前 record 里出现多少次。\n\n```text\nAskState appears 2 times in record A\nAskState appears 0 times in record B\n```\n\n出现次数越多，通常越相关。但次数增长不能无限放大，所以 BM25 会用 `k1` 控制词频收益。\n\n#### DF \u002F IDF\n\nDF 是 document frequency：一个词出现在多少个文档或记录里。\n\nIDF 是 inverse document frequency：越稀有的词越重要。\n\n例如：\n\n```text\n\"the\" appears in almost every record → low IDF\n\"AnswerFrameV1\" appears in very few records → high IDF\n```\n\n常见 BM25 IDF 形式：\n\n```text\nIDF(q) = log(1 + (N - df(q) + 0.5) \u002F (df(q) + 0.5))\n```\n\n其中：\n\n```text\nN     = 总记录数\ndf(q) = 包含词 q 的记录数\n```\n\n#### Length Normalization\n\n长文本天然包含更多词，如果不做归一化，长 chunk 可能更容易命中 query。BM25 用 `b` 和平均文档长度 `avgdl` 做长度归一化。\n\nBM25 常见公式：\n\n```text\nscore(D, Q) = Σ IDF(qᵢ) ×\n  f(qᵢ, D) × (k1 + 1)\n  \u002F\n  (f(qᵢ, D) + k1 × (1 - b + b × |D| \u002F avgdl))\n```\n\n其中：\n\n```text\nf(qᵢ, D) = 词 qᵢ 在文档 D 中出现次数\n|D|      = 当前文档长度\navgdl    = 平均文档长度\nk1       = 控制词频增长\nb        = 控制长度归一化\n```\n\n本章先使用默认教学参数：\n\n```text\nk1 = 1.5\nb = 0.75\n```\n\n\u003C\u002Fdetails>\n\n### 9. Hybrid Retrieval：Dense + Sparse 融合\n\n这一节解决的问题是：dense retrieval 擅长语义，BM25 擅长关键词；Klara 需要把两种检索结果合并成一个更稳的候选列表。\n\n```text\nquery_text + query_vector\n→ Dense Retriever + BM25 Retriever\n→ HybridRetriever\n→ HybridSearchResult[]\n```\n\n输入是用户 query 文本、query vector、dense results 和 BM25 results，输出是融合后的 `HybridSearchResult[]`。Klara 在这里学会：同时使用语义相似和关键词匹配，而不是只相信一种检索方式。\n\n对应代码：\n\n```text\nsrc\u002Fagent_ladder\u002Frag\u002Fretrieval\u002Fdense.py\nsrc\u002Fagent_ladder\u002Frag\u002Fretrieval\u002Fhybrid.py\nsrc\u002Fagent_ladder\u002Frag\u002Fretrieval\u002Fresult.py\n```\n\n\u003Cdetails>\n\u003Csummary>展开：Score Fusion、RRF 与混合检索取舍\u003C\u002Fsummary>\n\n#### 为什么要 hybrid\n\n只用 dense，可能漏掉精确术语；只用 BM25，可能漏掉语义改写。\n\n例如：\n\n```text\nquery: \"What did Klara gain in the first chapter?\"\n```\n\n这句话可能没有直接出现 `MinimalAgent`、`AskState`，但语义和第一章 chunk 很接近，dense retrieval 很有用。\n\n另一个问题：\n\n```text\nquery: \"Explain AnswerFrameV1\"\n```\n\n这里 `AnswerFrameV1` 是精确符号，BM25 很有用。\n\n所以 hybrid retrieval 需要把两者结合。\n\n#### 加权分数融合\n\n最直观的方式是加权：\n\n```text\nhybrid_score =\n  dense_weight × normalized_dense_score\n  + sparse_weight × normalized_bm25_score\n```\n\n例如：\n\n```text\ndense_weight = 0.7\nsparse_weight = 0.3\n```\n\n这样语义检索是主力，但关键词也能补充。\n\n#### 为什么要 normalize\n\ndense score 和 BM25 score 的范围不一定一样。Cosine similarity 通常在 `-1 ~ 1` 或 `0 ~ 1` 附近；BM25 分数可能大于 1，也可能随语料变化。因此融合前要归一化，否则某一边可能因为尺度更大而压过另一边。\n\n#### RRF：Reciprocal Rank Fusion\n\nRRF 不直接比较分数，而是比较排名：\n\n```text\nRRF(d) = Σ 1 \u002F (k + rankᵢ(d))\n```\n\n如果一个 chunk 在 dense 和 BM25 两边排名都靠前，它的融合分数就高。RRF 的优点是对不同检索器的分数尺度不敏感。\n\n本章第一版可以先用加权分数融合，因为它最容易理解；README 中保留 RRF，是为了让学习者知道真实系统常用 rank fusion。\n\n#### 输出不是最终答案\n\nHybrid retrieval 只是召回候选资料。它的输出仍然是 candidates，不是最终 evidence。最终还需要 reranking。\n\n\u003C\u002Fdetails>\n\n### 10. Reranking：从候选 chunk 里选证据\n\n这一节解决的问题是：Hybrid retrieval 会召回一批候选 chunks，但 Writer 不应该吃太多上下文；Klara 需要从候选中选出最值得进入 prompt 的证据片段。\n\n```text\nHybridSearchResult[]\n→ SimpleReranker\n→ RerankedChunk[]\n```\n\n输入是融合检索后的候选结果，输出是精排后的少量 `RerankedChunk[]`。Klara 在这里学会：粗排负责“找一批可能相关的”，精排负责“选出最适合回答当前问题的”。\n\n对应代码：\n\n```text\nsrc\u002Fagent_ladder\u002Frag\u002Freranking\u002Fsimple_reranker.py\n```\n\n\u003Cdetails>\n\u003Csummary>展开：粗排、精排、规则 rerank 与模型 rerank\u003C\u002Fsummary>\n\n#### Coarse Retrieval vs Reranking\n\n粗排的目标是召回：\n\n```text\n尽量不要漏掉可能相关的 chunk\n```\n\n精排的目标是选择：\n\n```text\n从候选里挑最适合进入上下文的 chunk\n```\n\n如果直接把 top 20 都塞给 Writer，会导致：\n\n```text\nprompt 太长\ntoken 成本变高\n无关信息干扰回答\n引用来源不清楚\n```\n\n所以需要 reranking。\n\n#### 本章的规则 reranker\n\n本章不先接 cross-encoder，也不让 LLM 做 judge。先用可解释的规则：\n\n```text\nfinal_score =\n  hybrid_score\n  + exact_keyword_bonus\n  + title_match_bonus\n  + tag_match_bonus\n```\n\n比如用户问 `RunLog`，包含 `RunLog` 的 chunk 可以加分；用户问 `chapter one`，metadata 里 `chapter: ch01` 的 chunk 可以加分。\n\n#### Cross-Encoder Reranker\n\nCross-encoder 会把 query 和 chunk 一起输入模型：\n\n```text\n(query, chunk) → relevance score\n```\n\n它通常比简单向量相似度更准，但速度更慢，成本更高。\n\n#### LLM Reranker\n\nLLM reranker 可以让模型判断哪些 chunks 能回答问题，但需要控制成本，也要避免模型“凭感觉”解释过度。本章先不用它，等后续 Agentic RAG 再引入 evidence grader \u002F verifier。\n\n\u003C\u002Fdetails>\n\n## Part D：问题进入 RAG 链路\n\nPart D 的目标是：让用户问题不再直接进入 Writer，而是先经过路由判断和上下文构建。\n\n```text\nUser Question\n→ Intent Router\n→ Direct or RAG\n→ Context Builder\n```\n\n### 11. Intent Router：判断是否需要 RAG\n\n这一节解决的问题是：不是所有问题都需要查资料。Klara 需要先判断用户是在普通聊天，还是在问需要本地知识库支撑的问题。\n\n```text\nUserQuestion\n→ IntentRouter\n→ RouteDecision\n```\n\n输入是用户问题，输出是结构化 `RouteDecision`。Klara 在这里学会：先判断是否需要进入 RAG，再决定是直接交给 Writer，还是启动检索链路。\n\n对应代码：\n\n```text\nsrc\u002Fagent_ladder\u002Frag\u002Fcontracts\u002Froute.py\nsrc\u002Fagent_ladder\u002Frag\u002Frouting\u002Fintent_router.py\n```\n\n\u003Cdetails>\n\u003Csummary>展开：Direct Answer、RAG Answer 与 RouteDecision\u003C\u002Fsummary>\n\n#### 为什么需要路由\n\n如果用户问：\n\n```text\n你好\n```\n\n不需要查本地知识库。\n\n如果用户问：\n\n```text\nWhat did Klara learn in chapter one?\n```\n\n就应该进入 RAG，因为答案依赖项目资料。\n\nIntent Router 的输出应该是结构化的：\n\n```text\nRouteDecision:\n  route: \"direct\" | \"rag\"\n  reason: string\n  confidence: float\n```\n\n#### 简单规则\n\nv0.2 可以先用规则：\n\n```text\n如果问题包含 chapter、Klara、Agent Ladder、AskState、RunLog、RAG 等项目词 → rag\n如果是普通寒暄或通用常识 → direct\n```\n\n后续可以升级成 LLM router 或小分类模型。\n\n#### 前端卡片\n\n右侧 Run Chain 可以显示一张卡片：\n\n```text\nIntent Router\n✓ Completed\nDecision: RAG\nReason: The question asks about Klara's chapter knowledge.\n```\n\n这张卡片只展示 public decision，不展示模型 chain-of-thought。\n\n\u003C\u002Fdetails>\n\n### 12. Context Builder：把证据组织给 Writer\n\n这一节解决的问题是：检索和精排得到的是多个 chunks，但 Writer 不应该直接吃原始 chunk 列表；Klara 需要把证据组织成稳定的上下文结构。\n\n```text\nRerankedChunk[]\n→ ContextBuilder\n→ BuiltContext\n```\n\n输入是精排后的 chunks，输出是 `BuiltContext`。Klara 在这里学会：控制 token budget、保留来源信息、用稳定格式把证据交给 Writer。\n\n对应代码：\n\n```text\nsrc\u002Fagent_ladder\u002Frag\u002Fcontracts\u002Fcontext.py\nsrc\u002Fagent_ladder\u002Frag\u002Fcontext\u002Fcontext_builder.py\n```\n\n\u003Cdetails>\n\u003Csummary>展开：为什么 Writer 不直接吃 chunks\u003C\u002Fsummary>\n\n#### 直接塞 chunks 的问题\n\n如果直接把 chunks 原样塞给 Writer，会有几个问题：\n\n```text\n顺序不稳定\n来源信息混乱\ntoken 不受控\n重复 chunk 可能进入 prompt\nWriter 不知道哪些字段是正文，哪些字段是 source\n```\n\n所以需要 Context Builder。\n\n#### BuiltContext\n\n`BuiltContext` 可以包含：\n\n```text\nquery\nselected_chunks\ncontext_text\ntoken_estimate\nsource_summaries\n```\n\n其中 `context_text` 是真正给 Writer 的内容，`selected_chunks` 则保留结构化来源，方便后面生成 citation。\n\n#### Token Budget\n\nv0.2 可以先用简单估算：\n\n```text\ntoken_estimate ≈ len(text) \u002F 4\n```\n\n如果超过预算，就减少 chunk 数量或截断较低分 chunk。后续 production 章节再接更准确 tokenizer。\n\n#### Prompt 格式\n\n上下文可以组织成：\n\n```text\n[Source 1]\nchunk_id: ...\ntitle: ...\ntext: ...\n\n[Source 2]\n...\n```\n\n这样 Writer 能明确知道每段资料来自哪里。\n\n\u003C\u002Fdetails>\n\n## Part E：从资料到答案\n\nPart E 的目标是：让 Klara 不只是找到资料，还能基于资料生成结构化答案，并说明来源。\n\n```text\nBuiltContext\n→ KlaraAgent Writer\n→ AnswerFrameV1\n→ SourceCard \u002F Citation\n```\n\n### 13. KlaraAgent Writer：基于证据回答\n\n这一节解决的问题是：RAG 检索出的上下文需要一个 Writer 来生成最终回答。第二章开始，第一章的 Minimal Agent 概念会演化成 `KlaraAgent`。\n\n```text\nUserQuestion + BuiltContext\n→ KlaraAgent Writer\n→ Draft Answer\n```\n\n输入是用户问题和 `BuiltContext`，输出是基于证据生成的回答草稿。Klara 在这里学会：不是只靠模型记忆回答，而是先阅读检索上下文，再用 Writer 生成答案。\n\n对应代码：\n\n```text\nsrc\u002Fagent_ladder\u002Fcore\u002Fruntime\u002Fklara_agent.py\nsrc\u002Fagent_ladder\u002Frag\u002Fwriter\u002Fklara_writer.py\n```\n\n\u003Cdetails>\n\u003Csummary>展开：MinimalAgent 如何演化成 KlaraAgent\u003C\u002Fsummary>\n\n第一章里的核心是 Minimal Agent：\n\n```text\nAskState\n→ LLM Call\n→ AnswerState\n→ RunLog\n```\n\n第二章不应该推翻它，而是让它升级：\n\n```text\nMinimalAgent = Chapter 1 的最小形态\nKlaraAgent   = 从 Chapter 2 开始的主 Agent\n```\n\n在 RAG 链路中，KlaraAgent 扮演 Writer：\n\n```text\nBuiltContext\n→ Writer Prompt\n→ LLM Call\n→ Answer Draft\n```\n\nWriter 不直接吃原始 chunks，而是吃 `BuiltContext`。这样后面可以替换成更复杂的 EvidencePack，也能让前端展示 Context Builder 和 Writer 两张不同卡片。\n\n#### 前端卡片\n\n右侧 Run Chain 可以显示：\n\n```text\nKlaraAgent Writer\n✓ Completed · 2.1s\nInput tokens: 1200\nOutput tokens: 340\n```\n\n展开后可以看到：\n\n```text\nInput: BuiltContext summary\nOutput: answer draft\nModel: qwen3.6-flash\n```\n\n\u003C\u002Fdetails>\n\n### 14. SourceCard \u002F Citation：答案从哪里来\n\n这一节解决的问题是：RAG 答案必须能说明资料来源，而不是只给一段看似正确的回答。\n\n```text\nSelected chunks + Answer Draft\n→ SourceCard[] + Citation[]\n```\n\n输入是被选中的 chunks 和回答草稿，输出是 `SourceCard[]` 与 `Citation[]`。Klara 在这里学会：回答不仅要有内容，还要能追溯到资料。\n\n对应代码：\n\n```text\nsrc\u002Fagent_ladder\u002Frag\u002Fcontracts\u002Fsource.py\nsrc\u002Fagent_ladder\u002Frag\u002Fcitations\u002Fsource_card.py\n```\n\n\u003Cdetails>\n\u003Csummary>展开：SourceCard、Citation 粒度与 Chapter 3 边界\u003C\u002Fsummary>\n\n#### SourceCard\n\n`SourceCard` 是给用户看的来源卡片。它可以包含：\n\n```text\nsource_id\ntitle\nsource_path\nchapter\nversion\nsummary\nused_chunk_ids\n```\n\n它回答：\n\n```text\n这次回答参考了哪些资料？\n```\n\n#### Citation\n\n`Citation` 更细，它绑定到具体 chunk：\n\n```text\ncitation_id\nchunk_id\nsource_id\nquote_or_summary\n```\n\n它回答：\n\n```text\n这句话或这段回答参考了哪个 chunk？\n```\n\n#### v0.2 的边界\n\nv0.2 可以先做简单 citation：把答案末尾列出 sources，或者在段落后放 `[source: ...]`。\n\n更细粒度的 claim-source mapping、citation verifier、证据一致性检查，放到 Chapter 3 Agentic RAG。\n\n\u003C\u002Fdetails>\n\n### 15. AnswerFrameV1：结构化答案\n\n这一节解决的问题是：RAG 的输出不应该只是一个字符串，而应该是一个结构化答案对象，方便前端、trace、eval 和后续章节复用。\n\n```text\nQuestion + Final Answer + Evidence\n→ AnswerFrameV1\n```\n\n输入是用户问题、最终回答和被选中的 evidence，输出是 `AnswerFrameV1`。Klara 在这里学会：答案对象保持轻量；运行信息、token、route、source card 和 citation 属于 RunLog \u002F module trace，而不是塞进最终答案本身。\n\n对应代码：\n\n```text\nsrc\u002Fagent_ladder\u002Frag\u002Fcontracts\u002Fanswer_frame.py\n```\n\n\u003Cdetails>\n\u003Csummary>展开：为什么答案不只是字符串\u003C\u002Fsummary>\n\n如果答案只是：\n\n```text\n\"Klara learned AskState and RunLog.\"\n```\n\n前端就很难知道：\n\n```text\n引用了哪些资料\n哪些 chunks 被使用\n是否走了 RAG\ntoken 花了多少\ntrace 保存在哪里\n```\n\n所以需要 `AnswerFrameV1`：\n\n```text\nquestion: string\nanswer: string\nevidence: EvidenceItem[]\n```\n\n这样前端可以渲染答案，右侧 Run Chain 可以通过 module trace 展示 route、retrieval、token、source card 和 citation 等运行信息。\n\n#### V1 的边界\n\nV1 不做复杂评估，不做 evidence verifier，不做 claim-level citation，也不保存 runtime metadata。它只保证：最终答案和被使用的 evidence 可以用一个稳定结构保存。\n\n\u003C\u002Fdetails>\n\n## Part F：前端 Run Chain 与章节冻结\n\nPart F 的目标是：把 RAG 的每一步以简单卡片显示在右侧，让用户看到 Klara 是如何从问题走到答案的。\n\n```text\nStructured module outputs\n→ Run Chain Cards\n→ Trace Summary\n```\n\n### 16. Run Chain Cards：前端如何展示 RAG 流程\n\n这一节解决的问题是：RAG 流程不能在前端变成一个黑盒。每个模块都应该像之前的 LLM Call 一样，成为右侧可展开的卡片。\n\n```text\nModuleResult[]\n→ Run Chain Cards\n→ Expandable Details\n```\n\n输入是每个模块的结构化输入输出，输出是右侧 Run Chain 卡片。Klara 在这里学会：把自己的运行过程用 public trace 展示出来，但不展示 chain-of-thought。\n\n对应代码：\n\n```text\napps\u002Fweb\u002Fsrc\u002Fcomponents\u002FRunMargin.tsx\napps\u002Fweb\u002Fsrc\u002Ftypes\u002Fdomain.ts\napps\u002Fapi\u002Froutes\u002Fruns.py\napps\u002Fapi\u002Fschemas.py\n```\n\n\u003Cdetails>\n\u003Csummary>展开：卡片结构、模块事件与 public trace\u003C\u002Fsummary>\n\n右侧卡片保持简单，和前面的 LLM Call 卡片一致：\n\n```text\nCard Title\nStatus\nSummary\nLatency\nInput Summary\nOutput Summary\nExpandable Details\n```\n\nv0.2 可以有这些卡片：\n\n```text\nIntent Router\nDense Retrieval\nBM25 Retrieval\nHybrid Retrieval\nReranking\nContext Builder\nKlaraAgent Writer\nRun Summary\n```\n\n每张卡片可展开看结构化输入输出。\n\n例如 Coarse Retrieval：\n\n```text\nInput:\nquery = \"What did Klara learn in chapter one?\"\n\nOutput:\n1. doc_ch01_minimal_agent_chunk_0002 score=0.82\n2. doc_global_klara_overview_chunk_0004 score=0.74\n```\n\nReranking 卡片可以显示：\n\n```text\nInput: 10 candidate chunks\nOutput: 3 selected chunks\n```\n\nSummary 最后显示：\n\n```text\nTotal latency\nInput tokens\nOutput tokens\nRetrieved chunks\nSelected chunks\nTrace saved\n```\n\n重要边界：前端只展示 public trace，不展示模型原始 chain-of-thought。\n\n\u003C\u002Fdetails>\n\n### 17. How to Run：本章最终运行方式\n\n这一节解决的问题是：读者完成 v0.2 后，应该能从本地知识库构建索引，启动 Klara，然后问一个会触发 RAG 的问题。\n\n```text\nKnowledge markdown\n→ Build local RAG index\n→ Start backend\u002Ffrontend\n→ Ask RAG question\n→ Inspect Run Chain cards\n```\n\n输入是 `data\u002Fknowledge\u002F` 下的 Markdown + metadata，输出是可检索的本地索引和一次可观察的 RAG run。Klara 在这里学会：把资料准备、检索、写作和前端 trace 串成一个完整体验。\n\n对应入口会固定为：\n\n```text\nscripts\u002Frag\u002Fbuild_index.py      # 构建本地 JSONL 索引，当前已实现\nstart.ps1                       # 启动前后端，当前已存在\napps\u002Fweb\u002Fsrc\u002Fcomponents\u002FRunMargin.tsx\n```\n\n\u003Cdetails>\n\u003Csummary>展开：v0.2 完成后的演示路径\u003C\u002Fsummary>\n\n最终演示路径应该很短：\n\n```text\n1. 准备知识文件\n   data\u002Fknowledge\u002Fglobal\u002Fklara-overview.md\n   data\u002Fknowledge\u002Fchapters\u002Fch01-minimal-agent.md\n   data\u002Fknowledge\u002Fchapters\u002Fch02-rag-agent.md\n\n2. 构建索引\n   py scripts\u002Frag\u002Fbuild_index.py\n\n3. 启动前后端\n   powershell -ExecutionPolicy Bypass -File .\\start.ps1 -NoOpen\n\n4. 提问\n   What did Klara learn in chapter one?\n\n5. 查看右侧 Run Chain\n   Intent Router\n   Dense Retrieval\n   BM25 Retrieval\n   Hybrid Retrieval\n   Reranking\n   Context Builder\n   KlaraAgent Writer\n   Run Summary\n```\n\n这条路径对应当前 v0.2 的完整演示：知识文件进入索引，问题进入 RAG，右侧 Run Chain 展示每一步结构化结果。\n\n\u003C\u002Fdetails>\n\n### 18. Known Limitations\n\n这一章的限制会保留在主线里，避免把 v0.2 做成过度复杂的生产系统。\n\n```text\nLocal JSONL index\nLLM JSON intent router with deterministic rule fallback\nSimple BM25\nSimple weighted hybrid fusion\nRule-based reranker\nBasic SourceCard \u002F Citation contracts\nNo citation verifier yet\n```\n\n这些限制是刻意保留的，因为本章目标是讲清楚基础 RAG 主线，不是一次性做完 Agentic RAG。\n\n### 19. 下一章：Agentic RAG\n\n下一章会进入 Agentic RAG。\n\n```text\nRAG\n→ query rewrite\n→ retrieval planning\n→ evidence selection\n→ citation verification\n→ insufficient evidence fallback\n→ state machine\n```\n\nv0.2 解决的是：Klara 如何基于本地资料回答。\n\nv0.3 要解决的是：Klara 如何主动规划检索、判断证据质量、处理多问题、多轮检索和不足证据。\n\n\n### 20. v0.2 实现顺序\n\n这一节解决的问题是：README 已经给出完整学习路线，代码实现要按最小模块逐步落地，不能一次性把 RAG 做成黑盒。\n\n```text\nRetrieval core\n→ Route + context\n→ KlaraAgent writer\n→ Run Chain cards\n→ Freeze\n```\n\n输入是前面已经写好的 contracts、loader、chunker、embedding 和 vector index，输出是一个能真实走通的 RAG Agent。Klara 在这里学会：每一步都以结构化对象传输，并且每一步都能在右侧 Run Chain 被看见。\n\n对应实现顺序：\n\n```text\n1. src\u002Fagent_ladder\u002Frag\u002Fretrieval\u002Ftokenizer.py\n2. src\u002Fagent_ladder\u002Frag\u002Fretrieval\u002Fbm25.py\n3. src\u002Fagent_ladder\u002Frag\u002Fretrieval\u002Fdense.py\n4. src\u002Fagent_ladder\u002Frag\u002Fretrieval\u002Fhybrid.py\n5. src\u002Fagent_ladder\u002Frag\u002Freranking\u002Fsimple_reranker.py\n6. src\u002Fagent_ladder\u002Frag\u002Frouting\u002Fintent_router.py\n7. src\u002Fagent_ladder\u002Frag\u002Fcontext\u002Fcontext_builder.py\n8. src\u002Fagent_ladder\u002Frag\u002Fcontracts\u002Fanswer_frame.py\n9. src\u002Fagent_ladder\u002Fcore\u002Fruntime\u002Fklara_agent.py\n10. apps\u002Fweb\u002Fsrc\u002Fcomponents\u002FRunMargin.tsx\n```\n\n\u003Cdetails>\n\u003Csummary>展开：为什么按这个顺序实现\u003C\u002Fsummary>\n\n实现顺序必须从“可检索”开始，而不是从前端开始。因为前端 Run Chain 要展示的是后端真实模块输出，不应该先写一套假 UI。\n\n#### 第一阶段：检索核心\n\n先完成：\n\n```text\nBM25\nDense retrieval wrapper\nHybrid fusion\nReranking\n```\n\n这样 Klara 可以从本地知识库里找到候选 chunks。\n\n#### 第二阶段：RAG 链路\n\n再完成：\n\n```text\nIntent Router\nContext Builder\nAnswerFrameV1\n```\n\n这样用户问题可以被路由，证据可以被组织，答案可以结构化返回。\n\n#### 第三阶段：KlaraAgent Writer\n\n`KlaraAgent` 不替代第一章的 `MinimalAgent`，而是在它上面增加 RAG 能力：\n\n```text\n如果 route = direct：\n  KlaraAgent → LLM Writer\n\n如果 route = rag：\n  KlaraAgent → Retrieval → Context → Writer → AnswerFrameV1\n```\n\n#### 第四阶段：前端 Run Chain\n\n前端只做一件事：展示真实模块结果。\n\n每张卡片对应一个结构化 module result：\n\n```text\nmodule_name\nstatus\nlatency_ms\ninput_summary\noutput_summary\ndetails\n```\n\n这样粗排、精排、Writer 都能用统一卡片展示，也方便后续 v0.3 加入 query rewrite、evidence grader、citation verifier。\n\n\u003C\u002Fdetails>\n","AgentLadder项目旨在通过Klara构建一个从大语言模型API到强化学习代理的桥梁，特别强调了RAG（Retrieval-Augmented Generation）技术的应用。其核心功能是让Klara能够基于本地知识库生成更加准确且可追踪的回答，包括读取文档、切分知识片段、生成embedding、建立向量索引等步骤，从而实现对特定问题的高效检索与回答。此外，该项目还设计了Metadata、意图识别、Context Builder等功能来辅助理解和处理信息。适合用于需要将私有或专业领域知识整合进对话系统中的场景，如企业内部知识管理、教育辅导等领域，使得AI助手不仅能够提供即时响应，还能确保答案的来源可靠性和透明度。",2,"2026-06-11 04:07:46","CREATED_QUERY"]