逐日AI
第 1 周 · D7约 5 小时

第一周综合:把六天的零件装成一个可一键启动的检索问答服务并复盘

把前六天的解析、切块、索引、检索、组装、生成拼成一个真正的服务:一条摄取命令、一个问答接口、一份配置说明,用 Docker 一键起来,然后回头复盘这一周每个决定的依据和留下的技术债。

今日目标 0/3

登录后可以勾选并保存进度。

今日目标

  1. 能把摄取与查询两条链路拆成清晰的模块边界,并说明每个模块可以被单独替换的理由
  2. 能用一条命令把服务连同数据库一起起来,并跑通从投喂文档到得到带引用回答的全过程
  3. 能列出这一周留下的三项技术债,并说明第二周分别在哪一天偿还

今天不学新原理。前六天你已经把六个零件各做了一遍,今天要做的是把它们装成一台能开门营业的机器,然后回头看看这一周的每个决定还站不站得住。读完回到页面顶部把三条目标勾掉。

小白版讲解

六个工种验收完了,图书馆还没开门

这一周我们一直在借图书馆打比方:语料是馆藏,切块是把书拆成词条,索引是卡片目录,检索是按目录找书,引用是论文末尾的参考文献。到今天为止六个工种的活儿都干完了——书运进来了、词条拆好了、目录做了关键词与向量两套、找书和标出处的规矩也都定了。

但图书馆还没开门,因为没有人能走进来。前六天每个 lab 都是一个跑完就退出的脚本:你敲一条命令,看一眼输出,然后关掉。真实世界里没人会这么用知识库,他要的是一个地址,把文档丢进去,然后随时能问。

从「六个脚本」到「一个服务」,中间隔着的不是代码量,是边界。脚本里所有东西都可以互相调用,因为反正一次性跑完就没了;服务里不行。服务要长期运行、要被别人改、要在第二周里被换掉一半的零件而不推倒重来。所以今天真正要做的决定只有一个:这台机器切成几块,切口在哪。

切口的判据不是「代码量差不多」,也不是「按功能分类」,而是这一句:

哪一层将来最可能被整个换掉,哪里就是切口。

按这条标准过一遍这一周,三刀的位置很清楚:embedding 模型一年里会换好几次,换一次意味着库里所有向量作废、全量重算;存储可能从 PostgreSQL 换成专用向量库,而且不管换不换,两条链路都得通过它说话;切块策略在第二周还要反复调。

第一刀先落在 embedding 上。接口就用 D2 定下的那个签名,一个字都不改:

embed.ts
// 全课统一的向量化接口:14 天都是这个签名,不许改名
export type Embedder = (
  texts: string[],
  opts?: { dimensions?: number }
) => Promise<number[][]>
 
export interface EmbeddingBackend {
  name: string // 写进 chunk_embeddings.model,用来判断库里的向量是不是当前模型算的
  dimensions: number
  embed: Embedder
}
 
export function createEmbedder(cfg) {
  // 三个后端,同一个签名:离线哈希、本地开源模型、调接口。
  // 上层代码永远不知道自己拿到的是哪一个
  if (useMock) return { name: `mock-hash-${dims}`, dimensions: dims, embed: hashBackend }
  return { name: `${cfg.model}-${dims}`, dimensions: dims, embed: openaiBackend }
}

注意这层抽象的理由不是「面向接口编程是好习惯」这种空话,而是非常具体的一件事:换模型一年会真的发生,每次你都只想改一个配置项、跑一次重建,而不是翻遍全仓库找调用点。代价同样具体——多一层间接,调试时要多跳一次。**值不值,看那件事会不会真的发生。**不会发生的别抽象,那叫过度设计。

两条链路,一个交界

划完刀口,下一个问题是:摄取和查询这两条链路,共享什么?

摄取是批处理:几十秒把三十篇文档解析、切块、向量化、写库,失败了重跑就行。查询是在线请求:几百毫秒内出结果,失败了用户当场看到。两者的错误处理与超时策略根本不是一回事。

最容易犯的错是看到「两边都要用 embedding」就抽一个公共模块,然后发现摄取想批量重试、查询想快速失败,模块里塞满了 if (isIngest)。**共享的应该是接口,不是流程。**这一版两条链路唯一共享的就是存储层那个接口:摄取往里写,查询往外读,此外不共享任何业务代码。

TextText
摄取链路                          查询链路
loadDocs   ← 解析(D3)           retrieve   ← 两路检索(D1 + D2)
  ↓                                 ↑
chunker    ← 切块(D4)           Store  ←──┘
  ↓                                 ↓
embed      ← 向量化(D2)         answer     ← 组装 + 带引用生成(D6)

Store ─────────────────────────────┘

embed 出现在两条链路上,但它是同一个接口的两次调用,不是共享的流程。这里有条硬约束:给块算向量和给问题算向量必须用同一个模型。用 A 模型建的索引拿 B 模型的问题去查,算出来的距离没有任何意义,而且不会报错,只会静静给你一堆看起来很像结果的垃圾。所以每条向量都要记下它是哪个模型算的(chunk_embeddings.model 这一列),这是唯一能在事后发现这类事故的办法。

存储层的接口写出来就这么几行,但它是整个服务里最该先定下来的东西:

store.ts
export interface Store {
  init(): Promise<void> // 可重复执行的迁移
  // 一篇文档连同它的块一次性替换。必须是一个事务:
  // 删了旧块却没写进新块,等于这篇文档从库里消失了
  replaceDoc(doc, chunks, vectors, model): Promise<void>
  listChunks(): Promise<StoredChunk[]>
  getChunk(chunkId: string): Promise<StoredChunk | null>
  vectorSearch(query: number[], topK: number): Promise<VectorHit[]>
}
 
// 唯一的选择点:有连接串就连真库,没有就用内存实现。
// 上面接口之外的代码,一行都不知道自己在跟谁说话
export function createStore(): Store {
  return process.env.DATABASE_URL ? new PgStore(process.env.DATABASE_URL) : new MemoryStore()
}

内存实现不是「假装写成功」的打桩,它把 PostgreSQL 那套语义真的重写了一遍:按文档覆盖旧块、余弦排序、同分时按 id 决胜,代价只是全表扫描。好处在第二周才显出来:D8 之后你每天要跑几十次检索做评估,如果每次都得先起容器,你就会开始偷懒不跑。

参数不进代码:配置化到底要配到哪一层

第二周会反复出现同一类问题:切成 500 字还是 300 字?取四条还是八条?关掉向量那一路会怎样?

这些问题只有一种正确的回答方式:**改一个配置,重跑一遍,看数字。**如果改切块大小要动 pipeline.ts、改检索路数要动 retrieve.ts,你一天最多试三种配置,而且试完就忘了自己试过什么。所以这一版把参数全收进一个 rag.config.json

JSONJSON
{
  "chunking": { "strategy": "heading", "maxChars": 500, "minChars": 60, "overlapChars": 60 },
  "embedding": { "backend": "auto", "model": "text-embedding-3-small", "dimensions": 384 },
  "retrieval": {
    "routes": ["bm25", "vector"],
    "perRouteTopK": 8,
    "topK": 4,
    "weights": { "bm25": 1, "vector": 0.6 },
    "gates": { "bm25MinScore": 6, "vectorMinCosine": 0.2 },
    "bm25": { "k1": 1.2, "b": 0.75 }
  },
  "generation": { "model": "claude-sonnet-5", "maxContextChars": 6000 }
}

四段分别对应四个可替换的零件,一眼就能看出这台机器有哪些旋钮。跑起来之后改一个环境变量就能临时覆盖,不用改文件:CHUNK_STRATEGY=fixed 会让同一份语料从 134 块变成 52 块,ROUTES=bm25 会关掉向量那一路。

有一条边界要划清楚:**环境变量只做覆盖,不做定义。**配置文件永远是那份「说明书」,看一个文件就知道有哪些参数、默认值是多少。反过来把默认值散在十几个 process.env.XXX ?? 某个数 里,结果是没人说得清系统到底跑在什么配置下,出了问题也复现不了。

配置里最值得停下来看的是 gates 这两个门槛:一条候选只要有任意一路的原始分过了自己那条线就算够得着,一条都没过就直接拒答。这里有个反直觉的坑——门槛必须卡在原始分上,不能卡在融合分上。融合分是按本次查询的最高分归一化出来的,哪怕全场最相关的那条只有 3 分,融合分照样是 1.0,拿它当门槛,系统就再也不会拒答了。

至于 vectorMinCosine 那个 0.2,我得说实话:**是量出来的,不是算出来的。**我把离线哈希向量的相似度分布打印了一遍,真正相关的块落在 0.2 出头(实测 0.20 到 0.27),而一个语料里根本没有答案的问题,它的最高分只有 0.13,就把线划在了 0.2。换成真实 embedding 分布完全不同,得重新标定;而怎么标定才算「对」,今天没有办法回答——这正是第一笔债。

三个接口,一个不能少

服务对外只有三个接口,少一个都不成立:

  • POST /ingest:给一个目录,解析、切块、向量化、入库,返回这次处理了几篇、切出几块。
  • POST /ask:给一个问题,返回带引用的回答、用到的块、以及没过门槛的候选
  • GET /chunks/:chunkId:给一个块编号,返回它的原文。

第三个最容易被砍掉,而它恰恰是这一整套东西的兑现。D6 让模型只输出本次发给它的块序号 [1] [2],代码回查确认这句话和那一块真的对得上,再把序号换成 doc-006#c04 这种能点开的编号还给用户。用户点不开,这一整套就跟一句「据我所知」没区别。引用的价值不在于标出来,在于能被查证。

返回体里还有个容易被砍的字段:没过门槛的候选。它对用户没用,对你有用——线上出现「怎么没答上来」时,第一件事就是看检索到底捞回了什么:压根没捞到(索引或分词的问题)、捞到了分数不够(门槛的问题)、还是分数也够但模型没用上(提示词的问题)。三种故障修法完全不同,没有这个字段你连分类都做不到。

摄取接口有个必须做对的细节。关键词那一路的 BM25 索引建在内存里,所以摄取完必须重建索引

index.ts
app.post('/ingest', async (req) => {
  const report = await ingest(dir, store, cfg, embedder)
  // 摄取完必须刷新关键词索引,否则新入库的块「向量能查到、关键词查不到」。
  // 这种一路能查一路查不到的故障最难排,因为它看起来只是「结果怪怪的」
  const indexed = await retriever.refresh()
  return { ...report, indexed } // 返回 indexed 就是为了能验证这件事真的做了
})

一条命令起来:迁移到底谁来跑

「一键启动」听起来是运维的事,其实它决定了你一天能试几次。起环境要五分钟、要记六条命令的项目,你一天只跑两次;docker compose up -d 就完事的,你一小时能跑十次。第二周每天都要重建索引、重跑评估,这个差距会被放大几十倍。

数据库这边要准备三样东西:装 pgvector 扩展、建三张表、建向量索引。问题是谁来跑、什么时候跑。最省事的做法是把建表脚本挂进容器的初始化目录,第一次起库时自动执行:

YAMLYAML
services:
  postgres:
    image: pgvector/pgvector:pg16
    ports:
      - '5557:5432'
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U rag -d rag']
      interval: 5s
      retries: 20
    volumes:
      - ./migrations:/docker-entrypoint-initdb.d:ro
      - pgdata:/var/lib/postgresql/data

这里有个坑值得单独记住:**初始化目录里的脚本只在数据卷为空时执行一次。**改了建表语句再重启容器,新语句一句都不会跑,然后你对着「column does not exist」查半天。所以服务端启动时还要跑一遍等价的、全部写成 IF NOT EXISTS 的建表语句兜底。两处看着重复,解决的却是两个场景:一个全新环境,一个已有数据的环境。

healthcheck 也不是装饰:容器「启动了」和「能接受连接」之间有好几秒,没有它服务会在这几秒里连接失败退出,而你会以为是配置错了。

复盘:这一周的六个决定,现在看还成立吗

现在把这一周的决定一条条摊开,问同一个问题:当时依据什么,现在看还站得住吗?

**D1 先做纯 BM25 不碰向量。**理由是可解释、零依赖、能当基线。现在看价值比预想的还大——今天的自测能把九个问题的结果跟 D1 并排打出来,一眼看出哪些命中变了。**没有基线的优化全是自我感觉。**要修正的是那句「关键词会被向量取代」的潜台词:并没有,今天关键词那一路依然贡献了绝大多数命中。

**D2 主线用接口版 embedding,本地模型作兜底。**理由是省事,现在依然成立,但补一条当时没想到的:正因为它随时可能换,才逼出了那个统一接口,而这个接口今天成了整个服务里最干净的一层。

**D3 把解析产物统一成带标题路径的节点序列。**这是回报最高的一个决定,而且来得比预期早。今天按标题切块直接用上了 headingPath,块的正文里能带上定位说明,引用展示也能显示它出自哪一节——都是那个数据结构白送的。

**D4 用命中率而不是直觉选切法。**这一条今天很尴尬:我选了「按标题切」,却给不出严格证据。量到的只是块数从 52 变成 134,以及九个基线问题里有五个的命中文档集合跟着变了。**变好还是变坏,我说不出来——这不是评估,这是观察。**证据要等 D8。

**D5 留在 PostgreSQL,先不上专用向量库。**一百多个块连近似索引都用不上,顺序扫比 HNSW 还快还准;索引那一行照样写进迁移脚本,因为数据量涨上来的那天它必须已经在那儿。前提写得很清楚:数据量、写入频率、过滤复杂度、运维能力,任何一条越界都要重估。

D6 引用必须由代码回查,不信模型自觉。今天的自测专门喂了两条坏引用进去:一条编号越界,一条编号是真的但内容对不上。前者好抓,后者只有算过重合度才发现。它的分量在于这是唯一一条不能靠提示词兜住的规则:提示词能让模型「更倾向于」不编,只有代码回查能保证编了的进不了返回结果。

一句话总结:六个决定里五个站得住,站不住的那个不是选错了,是因为没有秤。

留给第二周的三笔债

一个诚实的里程碑必须自己列出它没做到的事。这一版有三笔债,每一笔都有明确的还款日:

**第一笔:没有评估,所有配置都是拍的。**切块 500 字、取 4 条、门槛 0.2、权重 1 比 0.6——没有一个数字有证据。更糟的是现在改任何一个参数,我都说不出是变好还是变坏,只能说「命中的文档变了」。**这是最重的一笔,因为它让另外两笔都没法验收。**D8 会建标准答案集、实现召回率与排序指标,把今天这套配置的基线量出来;在那之前所有「我觉得更好」都只是猜测。

**第二笔:两路是并列跑的,融合方式很粗糙。**各自按最高分归一化再加权相加,对「本路最高分本身就很低」的情况完全是瞎的,而且没有重排,前四条全靠融合分决定。D9 会换成倒数排名融合(只看名次不看分数,正好绕开量纲问题)再接交叉编码器精排,并用 D8 的评估证明每一步各贡献了多少。

**第三笔:没有权限,也没有增量。**POST /ingest 是全量重跑,三十篇没问题,三万篇就是几小时和一笔真金白银的向量化账单。documents 表里的 content_hash 今天一次都没被读过,它是给 D13 留的伏笔。权限更直接:块上带着 department 却压根没用——在生成阶段才过滤等于已经泄露,材料早就进了上下文。D13 会把过滤下推到检索查询里,再补上缓存分层与链路追踪。

另外两处小的记在这里免得忘:BM25 索引建在单机内存里,多实例部署会不一致;摄取是串行的,没有并发也没有失败重试。

源码导读

动手实验

🧪 D7 实验:一个可用 Docker 一键启动、含摄取命令与带引用问答接口的完整 RAG 服务

代码位置:labs/rag-14days/day-07-rag-service

验收标准:

  1. 离线自测在参考答案里八项全部通过、退出码为 0,在原样的起始代码里有五项失败。
  2. 摄取那一项打印「30 篇 → 134 块」,且关键词索引的块数与入库块数相等,说明摄取完索引真的刷新了。
  3. 问「单个文件最大能上传多大」时,两篇结论打架的文档同时进上下文,每条引用都能用块编号接口点开看到原文。
  4. 问「公司每年团建预算是多少」走到拒答;喂给引用校验的两条坏引用——编号越界的、以及编号真实但内容对不上的——都被拦下。
  5. 起了容器数据库再跑一遍,八项结果与内存版完全一致;与第一天基线并排的九行里,拒答判断九比九一致。

今天的实验是第一周的里程碑,代码量是前几天的两三倍,但新逻辑很少——大部分是把前六天的东西按接口重新码放。四个练习点落在切块、向量化、检索门槛、引用校验上,每做完一个都能在自测里看到一项从叉变成勾。不用装 Docker、不用 API key 也能跑完整条链路:没有数据库连接串时存储自动退回内存实现,MOCK=1 把向量换成确定性哈希、把生成换成模板拼接。

  1. 把前六天的模块按八个文件的边界码放好,先把存储层那个接口定下来,再填两个实现,运行自测确认服务能起来。
  2. 补完切块练习:摄取那一项的块数从 30 变成 134,说明按标题层级切真的生效了。
  3. 补完向量化与门槛两个练习:向量那一项算出非零的相似度,「团建预算」那一问走到拒答。
  4. 补完引用校验练习:喂两条坏引用进去,看编号越界的那条和内容对不上的那条都出现在被拦下的列表里。
  5. 起容器数据库、加上连接串再跑一遍自测,确认八项结果与内存版一致,并把与第一天基线并排的九行抄进你自己的笔记。

面试题

今天 4 道题在下方题库区,侧重模块边界、配置化与可替换性,以及对第一周所有决定的复盘。展开后先看「分析过程」再看要点——照着推导练比背要点管用。

检查清单与明日预告

  • 能把摄取与查询两条链路拆成清晰的模块边界,并说明每个模块可以被单独替换的理由
  • 能用一条命令把服务连同数据库一起起来,并跑通从投喂文档到得到带引用回答的全过程
  • 能列出这一周留下的三项技术债,并说明第二周分别在哪一天偿还
  • 能说出「切口画在哪」的那条判据,并用它解释为什么向量化那一层必须可替换
  • 能解释为什么检索的门槛必须卡在每一路的原始分上而不是融合分上
  • 实验的 5 条验收标准全部通过
  • 4 道面试题不看要点也能答出至少 3 道

明天(D8)我们把秤造出来:出一份二十题的标准答案集,实现召回率、平均倒数排名与归一化折损累计增益三个指标,再用模型当裁判评忠实度,最后给今天这个服务跑出基线报告。顺序是故意的——评估要有东西可评,今天这台机器就是被评的对象。从明天起,这周留下的每一笔债还没还,都由数字说了算。

面试题库

  • 你会怎么划分一个检索增强生成系统的模块边界?其中哪一层最应该做成可替换的,为什么?How would you draw the module boundaries of a RAG system, and which layer most needs to be swappable? Why?
    国内高频海外高频基础#architecture#modularity#embeddings

    分析过程 · 先想清楚再作答

    1. 这题考的是你有没有真的维护过这类系统。只按「解析、切块、检索、生成」复述一遍流程图,面试官会判定你只搭过 demo——流程图人人都会画,切口画在哪才是经验。
    2. 给一条可复用的判据再往下推:切口应该落在「将来最可能被整个换掉」的地方,而不是按代码量或者功能名称均分。
    3. 用它过一遍:embedding 一年会换好几次,换一次库里所有向量作废、必须全量重算,所以它必须是接口;存储可能从 PostgreSQL 换成专用向量库,而且摄取和查询都要通过它,所以它是两条链路的唯一交界;切块策略在调优期天天改,所以它必须是配置项而不是硬编码。
    4. 结论:最该做成可替换的是 embedding 那一层,理由不是「设计模式」,而是「换模型这件事真的会发生,且发生时代价极高」。
    5. 顺手点出抽象的代价:每多一层间接就多一次跳转和一份心智负担,所以判据是「那件事会不会真的发生」,不会发生的别抽象。
    6. 可预期的追问:那生成模型要不要也抽象?答案是要,但优先级低——换生成模型不需要重算任何存量数据,回滚也便宜,所以它是配置项而不是一层接口。

    How to reason about it · think before answering

    1. This question separates people who have maintained such a system from people who have only built a demo. Reciting the pipeline diagram is not an answer; where you cut it is.
    2. Offer a reusable criterion first: cut where a layer is most likely to be replaced wholesale, not by lines of code or by tidy functional names.
    3. Apply it. Embedding models change several times a year, and each change invalidates every stored vector, so that layer must be an interface. Storage may move from PostgreSQL to a dedicated vector database, and both ingestion and query talk through it, so it is the single shared boundary. Chunking changes daily during tuning, so it belongs in config, not in code.
    4. Conclusion: the embedding layer is the one that must be swappable, because the swap is both likely and expensive, not because interfaces are good style.
    5. Name the cost of abstraction too: every indirection is one more hop while debugging, so the test is whether the change will actually happen.
    6. Expected follow-up: should the generation model be abstracted as well? Yes, but at lower priority, because swapping it does not force recomputation of stored data and rollback is cheap. It is a config value, not a layer.

    答题要点

    • 先给判据:切口落在最可能被整体替换的那一层,不按代码量或功能名称均分。
    • embedding 是最该抽象的一层:换模型意味着存量向量全部作废、必须全量重算,代价高且真的会发生。
    • 存储层是摄取与查询唯一的交界,接口要先定下来再谈两边实现。
    • 切块与检索路数做成配置项,因为它们在调优期改动最频繁,改一次不该动代码。
    • 抽象有成本,判据是那件事会不会真的发生;不会发生的抽象就是过度设计。

    Key points

    • Lead with the criterion: cut where a layer is most likely to be replaced wholesale.
    • The embedding layer is the one to abstract: swapping models invalidates every stored vector and forces a full recompute.
    • Storage is the single boundary shared by ingestion and query, so define its interface before either implementation.
    • Chunking and retrieval routes belong in configuration because they change most often during tuning.
    • Abstraction costs indirection, so only abstract changes that will actually happen.
  • 摄取链路和查询链路应该共享哪些代码?强行复用会带来什么具体问题?What should the ingestion path and the query path share, and what concretely goes wrong when you over-share?
    国内高频海外高频进阶#architecture#ingestion#retrieval

    分析过程 · 先想清楚再作答

    1. 题眼在「强行」两个字。面试官想看的是你能不能说出复用的边界,而不是背诵「不要重复自己」。
    2. 先说清两条链路的性质差异:摄取是批处理,几十秒跑完,失败重跑一遍就行;查询是在线请求,几百毫秒要出结果,失败用户当场看到。错误处理、超时、并发策略天然不同。
    3. 所以结论是:**共享接口,不共享流程**。两边唯一该共享的是存储层的那个接口,以及 embedding 的函数签名——注意后者共享的是签名和模型选择,不是调用流程。
    4. 给出强行复用的具体症状:抽出来的公共模块里开始出现 isIngest 这类分支,一个改动要同时验证两条链路,最后没人敢动它。
    5. 补一条真正必须一致的东西:给块算向量和给问题算向量必须用同一个模型。这不是复用代码,是复用配置——而且要把模型名写进向量表,否则模型换了没人发现,检索会静默地返回垃圾。
    6. 可预期的追问:那切块逻辑呢?查询侧压根不切块,所以它只属于摄取链路;真要在查询侧用到(比如 D11 的父子回填),走的也是存储层读回大块,不是把切块器搬过来。

    How to reason about it · think before answering

    1. The word to notice is 'over-share'. The interviewer wants the boundary, not a recital of DRY.
    2. Start from how the two paths differ. Ingestion is batch: tens of seconds, and a failure just means rerunning it. Query is online: hundreds of milliseconds, and a failure is visible to the user immediately. Error handling, timeouts and concurrency are simply not the same problem.
    3. Hence the rule: share the interface, not the flow. The only genuinely shared thing is the storage interface, plus the embedding function signature.
    4. Name the symptom of over-sharing: the extracted module fills up with isIngest branches, every change has to be verified on both paths, and eventually nobody dares touch it.
    5. Add the one thing that truly must match: chunks and queries must be embedded by the same model. That is shared configuration, not shared code, and the model name belongs in the vector table so a silent mismatch is detectable.
    6. Expected follow-up: what about chunking? The query path never chunks. Even when it needs a parent block, it reads it back through storage rather than importing the chunker.

    答题要点

    • 共享接口不共享流程:唯一的交界是存储层,加上 embedding 的函数签名。
    • 两条链路的错误处理与延迟约束根本不同,批处理可以重跑,在线请求必须快速失败。
    • 强行复用的症状是公共模块里长出 isIngest 分支,改一次要验两条链路。
    • 必须一致的是模型选择而不是代码:块与查询要用同一个 embedding 模型,并把模型名记进向量表。
    • 切块只属于摄取;查询侧需要大块时通过存储层读回,而不是把切块器搬过去。

    Key points

    • Share the interface, not the flow: storage is the only boundary, plus the embedding signature.
    • The two paths have different error handling and latency budgets; batch can rerun, online must fail fast.
    • Over-sharing shows up as isIngest branches and changes that must be verified twice.
    • What must match is the model choice, not the code: record the model name alongside every stored vector.
    • Chunking belongs to ingestion only; the query path reads larger units back through storage.
  • 一个检索问答服务上线前你会做哪三项检查?为什么偏偏是这三项?What three checks would you run before shipping a retrieval QA service, and why those three?
    国内高频海外高频进阶#production-readiness#citations#refusal

    分析过程 · 先想清楚再作答

    1. 这题的区分度不在你能列几项,而在你能不能说清「为什么是这三项」。列十项而每项都不给理由,反而说明你没有排过优先级。
    2. 推导方式是按后果排序:哪种故障用户看不出来、又损失最大,哪一项就该排在前面。
    3. 第一项是引用可查证:每条引用的编号都能回查到真实存在的块,且那一块确实与该句有实质重合。这一项排第一是因为引用错了用户根本发现不了,而它恰恰是这类系统唯一的信任来源。
    4. 第二项是该拒答时真的拒答:构造一个语料里没有答案的问题,看它是回那句拒答话术还是开始编。这一项也属于用户看不出来的故障,且一旦编造被发现,整个系统的可信度归零。
    5. 第三项是摄取到检索的一致性:摄取完之后新文档立刻能被检索到,且关键词与向量两路的覆盖数量对得上。这一项防的是「一路能查一路查不到」这种最难排查的故障。
    6. 可预期的追问:为什么延迟和成本不在前三?因为它们是**看得见**的故障——慢了用户会抱怨,贵了账单会告诉你;而上面三项不检查就永远不会有人告诉你。

    How to reason about it · think before answering

    1. The discriminator is not how many checks you list but whether you can justify the three. Ten items with no ranking suggests you have never had to prioritise.
    2. Derive them by consequence: the failures that are invisible to users and most damaging go first.
    3. First, citations must be verifiable: every cited id resolves to a real chunk, and that chunk genuinely overlaps the sentence citing it. This ranks first because a wrong citation is undetectable by the user, and citations are the only source of trust this system has.
    4. Second, refusal must actually fire: ask a question the corpus cannot answer and confirm the system says so instead of inventing. Also invisible, and one discovered fabrication zeroes out trust in the whole product.
    5. Third, ingestion-to-retrieval consistency: freshly ingested documents are retrievable immediately, and the keyword and vector paths cover the same set. This guards against the 'one route finds it, the other does not' failure, which is the hardest to diagnose.
    6. Expected follow-up: why not latency and cost? Because those failures are visible. Users complain about slowness and the bill reports overspending; nobody will ever report the three above.

    答题要点

    • 先给排序依据:优先检查用户发现不了、但后果最重的故障。
    • 第一项引用可查证:编号能回查到真实的块,且该块与被引的那句话有实质重合。
    • 第二项拒答生效:用一个语料里没有答案的问题验证系统会说查不到,而不是开始编。
    • 第三项摄取与检索一致:新入库的文档立刻可检索,关键词与向量两路覆盖对得上。
    • 延迟和成本重要但排在后面,因为它们是看得见的故障,会自己找上门。

    Key points

    • State the ranking rule first: prioritise failures users cannot see but that cost the most.
    • Check one, verifiable citations: every id resolves to a real chunk that overlaps the sentence citing it.
    • Check two, refusal actually fires on a question the corpus cannot answer.
    • Check three, ingestion and retrieval agree: new documents are immediately retrievable on both routes.
    • Latency and cost matter but rank lower because those failures announce themselves.
  • 你刚拼出来的这个检索问答系统,现在最大的风险在哪里?你打算怎么证明这个判断?What is the biggest risk in the RAG service you just assembled, and how would you prove that judgment?
    国内高频海外高频深入#evaluation#risk-assessment#retrospective

    分析过程 · 先想清楚再作答

    1. 这题有两半,后半句才是题眼。说出一个风险不难,难的是给出一个能证伪你自己判断的方法——答不出后半句,前半句就只是意见。
    2. 先排除两个常见的错误答案:说「幻觉」太笼统,没有指向任何可动的地方;说「延迟」则是把看得见的问题当成最大风险。
    3. 真正的最大风险是**没有评估**:切块大小、取几条、门槛定多少、两路怎么加权,全是拍出来的。它最重要的地方在于它让所有其他风险都无法验收——你连「改了之后变好还是变坏」都说不出口。
    4. 怎么证明:先从语料反向出一份带标准答案文档的问题集,刻意掺进无答案问题和需要跨文档的多跳问题;再实现召回率与排序指标,给当前配置跑出一个基线;然后把一个参数来回改两次,看指标动不动。如果指标对参数完全不敏感,说明是评估集有问题,不是系统没问题。
    5. 补一句成本口径:每一项优化都要同时报三笔账——指标涨了多少、延迟涨了多少、钱涨了多少。只报第一笔的结论不能用。
    6. 可预期的追问:评估集多大才够?先做二十题能覆盖主要问题类型的小集,用它挡住明显的退步;等真实用户问题攒起来,再按真实分布扩到几百题。一上来就追求规模,只会得到一堆自己出的、跟真实用法无关的题。

    How to reason about it · think before answering

    1. There are two halves here and the second is the real question. Naming a risk is easy; giving a method that could falsify your own claim is what separates answers from opinions.
    2. Rule out two common wrong answers: 'hallucination' is too vague to act on, and 'latency' mistakes a visible problem for the biggest one.
    3. The biggest risk is the absence of evaluation. Chunk size, top-k, thresholds and route weights were all guessed, and that makes every other risk unverifiable: you cannot even say whether a change helped.
    4. How to prove it: build a question set from the corpus with known answer documents, deliberately including unanswerable and multi-hop questions; implement recall and ranking metrics; produce a baseline for the current configuration; then move one parameter back and forth and watch whether the metrics move. If they do not move at all, the evaluation set is wrong, not the system.
    5. Add the accounting rule: every optimisation reports three numbers, metric gain, latency added and cost added. A claim with only the first is not usable.
    6. Expected follow-up: how large must the set be? Start with roughly twenty questions covering the main question types to catch obvious regressions, then grow toward the real distribution once you have actual user questions. Chasing size first only yields questions you invented yourself.

    答题要点

    • 最大的风险是没有评估:所有参数都是拍的,导致任何改动的好坏都无法判断。
    • 证明方式是先建标准答案集,刻意包含无答案问题与多跳问题,再跑出当前配置的基线。
    • 用参数扰动反过来验证评估集本身:指标对参数完全不敏感,说明题出得有问题。
    • 每项优化同时报三笔账:指标、延迟、成本;只报指标的结论不能用。
    • 评估集先小而全,覆盖问题类型即可,等真实问题攒起来再按真实分布扩大。

    Key points

    • The biggest risk is having no evaluation: every parameter was guessed, so no change can be judged.
    • Prove it by building a golden set with known answer documents, including unanswerable and multi-hop questions, then baseline the current configuration.
    • Validate the set itself by perturbing parameters: metrics that never move mean the questions are wrong.
    • Report three numbers per optimisation: metric gain, added latency, added cost.
    • Start small but well covered, then grow toward the real question distribution.

评论

登录后即可参与讨论

还没有评论,来说第一句。