从零搭出能回答"自己文档"的 RAG:179 行代码,成本不到 1 毛
为什么做这个项目
我是研一学生,目标是 2027 年暑期实习。一直想做点"能真正回答问题"的 AI 项目,而不是 demo 级的调用示例。VisionRAG 的设想是做一个图文混合的检索问答系统:企业里有大量图文混排文档,传统关键词检索找不到图,纯文本 RAG 又丢图像信息——最终产品要让每个答案都能溯源到具体图片和段落。
这个目标太远,所以第一周我只做一件事:先让 RAG 跑起来(纯文本版 v0)。这篇文章记录这 5 天(实际用了 3 天)的过程、代码和踩过的坑。
什么是 RAG
RAG(Retrieval-Augmented Generation,检索增强生成)解决大模型的一个老毛病:知识停在训练时点,新资料它不知道,还会瞎编。做法是:先把资料切块、转成向量存起来(建索引);用户提问时,把问题也转成向量,找出最相关的几个片段;再把片段和问题一起交给大模型,让它"看着资料"回答。
五步链路:加载 → 分块 → 嵌入 → 检索 → 生成。
5 天过程
- D1:uv 装环境、git 建仓、配 .env、第一次调通 DeepSeek API
- D2:把项目拆成 config.py / llm.py / main.py 三层,首次跑通对话
- D3:流式输出、温度参数、API 成本计费、防崩兜底、多轮记忆——还抓出一个"每句话调两次 API"的烧钱 bug
- D4:写最小 RAG——4 篇短文分块嵌入建索引,余弦相似度检索,3 个测试问题 top-1 全部命中
- D5:把检索片段拼进提示词生成答案,v0 端到端跑通——问"今天天气"它会老实说不知道
- D6:整理结构、写 README、补 .env.example(还修了一个 gitignore 的坑)
技术核心:三段加起来不到 40 行
嵌入(embedding)就是把文字变成一串数字(向量),我管它叫"语义身份证":意思相近的句子,向量方向也接近。检索 = 拿问题的身份证,跟所有候选比方向像不像。
余弦相似度衡量两个向量方向有多像,公式就三行:
def cos_sim(a: list[float], b: list[float]) -> float:
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a))
nb = math.sqrt(sum(y * y for y in b))
return dot / (na * nb)
生成环节的核心是提示词组装——把检索到的片段拼进模板,再让模型回答:
prompt = f"""请基于以下资料回答用户问题。如果资料里没有答案,请直接说不知道。
资料:
{context}
问题:{question}"""
最后那句话是关键:"没有答案就说不知道"。实测问"今天上海天气怎么样",模型回答"不知道。资料中没有上海天气的信息。"——RAG 系统最重要的品行不是"能答",而是"不乱编"。
踩过的坑(全是真金白银)
1. 每句话被调了两次 API(最烧钱):给聊天加防崩代码时,旧的调用没删,导致一句话被调两次 API——回答出现两份,花费 double。排查套路:现象(两次回答)→ 假设(两次调用)→ 读代码证实(run() 里有两个 stream_chat)。
2. 把 >>> 敲进了 PowerShell:终端和 Python 解释器是两回事。PS> 开头是操作系统命令;先输入 python 进入解释器,出现 >>> 才能敲 Python 代码。看起来弱智,但第一天真踩了。
3. .env.example 被 gitignore 吞了:.env(真密钥,不能上传)和 .env.example(空模板,应该上传)长得太像,规则 .env.* 把模板也误杀了。加一行 !.env.example(感叹号 = 例外)救回来。教训:"文件存在" ≠ "文件会被提交",改完 .gitignore 必查 git status。
4. 零宽空格(U+200B):一个看不见的字符:从网页复制代码时会带进一个不可见字符,导致 if __name__ == "__main__": 永远为 False。踩了两次才学会排查:len('__main__') 应该是 8,多 1 就是有脏字符。现在关键代码一律手敲。
数据说话
- 代码量:179 行(8 个文件),完整跑通一个 RAG 问答
- git 提交:11 次,每次提交都是一天的边界
- 欠债清单:21 条——遇到不懂的先记下来,S2 阶段集中清偿。这是我学到的工程习惯:不装懂
- 检索质量:3 个测试问题 top-1 全部命中(相似度 0.66 / 0.77 / 0.73)
- API 成本:不到 1 毛(约 0.02~0.05 元,口径:按未命中缓存计费估算)
下一步
- v1:检索质量优化——混合检索(关键词 + 向量)、Rerank、加评测(Recall@k、Faithfulness)
- v2:多模态——图像索引 + VLM 问答
- v3:服务化——FastAPI + Docker + 追踪
结尾
代码在 GitHub:github.com/huruxia/vis… 踩坑全集(DEBT.md):github.com/huruxia/vis…
感谢看完。有任何问题欢迎交流,尤其是分块策略和检索评测——那是 RAG 里最值得实验的部分。
本文由 AI图鉴自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。
RAG:179 line code from zero to answer "self-file" at less than 1 cent.
Why did you do this?
I'm a student with a target summer internship in 2027. Always trying to do an AI project that can really answer questions, not a demo level call example. VisionRAG envisages a search question-and-answer system with a mix of graphics: there are a lot of graphics in the enterprise, traditional keyword retrievals cannot be found, plain text RAGs are missing image information - the final product is to trace each answer to specific pictures and paragraphs.
This target is too far, so I'm only doing one thing in the first week: let RAG run. This article records the process, the code and the pits that were stepped on for five days.
What's RAG?
RAG (Retrieval-Augmented General, Retrieval Enhanced Generation) addresses one of the old problems of LLM: knowledge stops at training hours, new information is not known and can be made up. The approach is to cut the information and turn it into a vector (indexing); to turn the question into a vector when asked by the user and to identify the most relevant clips; and to turn the clip and the question together to LLM, so that it can "look at the information" and answer.
Five-step link: Load the poignant fractions embedded in the poaching search generation.
5-day process
- D1:uv for the environment, git for the warehouse,.env for the first call from DeepSeek API
-D2: Dismantling the project to config.py / lllm.py /main.py 3rd floor, first run through dialogue
-D3: Flow output, temperature parameters, API costing, crash bottoms, multiple rounds of memory - and a "twice API" burn money, bug
-D4: Write minimum RAG - 4 short sections embedded in index, cosine similarity search, 3 test questions top-1 all hit
-D5: Scroll the search footage into prompt to generate the answer, v0 to run through -- ask "Today's Weather" and he'll honestly not know.
.env.example.
Technical core: 3 sections added up to 40 lines
Embedding is turning words into numbers, and I call them semantic IDs: sentences that are similar in meaning, and direction is close. Retrieving = I.D. with problems, unlike all candidates.
The cosine similarity measures how similar the two vectors are, and the formula is three lines:
def cos_sim(a: list[float], b: list[float]) -> float:
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a))
nb = math.sqrt(sum(y * y for y in b))
return dot / (na * nb)
At the heart of the generation chain is the prompt assembly -- collating the retrieved fragments into the template, allowing the model to answer:
Please answer the user's question based on the following information. If there is no answer in the information, please state that you do not know.
Information:
{context}
Question
The last sentence is the key: "Don't know without an answer." "How's the weather today in Shanghai?" The model answers. There is no information on Shanghai weather. The most important behavior of the RAG system is not to be able to answer, but not to be made up.
It's all real silver.
Each sentence has been transferred twice to API (most expensive): when the crash code is added to the chat, the old call has not been deleted, resulting in the sentence being transferred twice to API — two answers have appeared, and the cost is due. Check the circuit: phenomena (twice answers) assumed (twice calls) read code confirmation (run()) with two stream chat).
2. Knocking > > into PowerShell: Terminal and Python interpreter is different. PS> starts with an operating system command; the python enters the interpreter and appears > before the Python code. Looks retarded, but it was the first day.
3..env.example was swallowed by.env (real key, not uploadable) and.env.example (empty template, should be uploaded), and rules.env.* killed the template by mistake. Add line!.env.example (Extraordinary). Lesson: "The file exists" ≠ "The file will be submitted". Gitignore will have to check git status.
4. Zero width spaces (U+200B): an invisible character: if name = = " main ": always False when copying a code from a web page. I stepped on it twice to learn how to screen: llen should be eight, more than one, with dirty characters. Now all key codes are knocked on.
Data Talk
- Volume: 179 lines (8 files) running through a full RAG question and answer
- prompt Submission: 11 times, every submission is a one-day boundary
- List of debts: 21 - Write down if you do not understand it and concentrate on the S2 phase. That's what I learned about engineering: I don't know.
- Retrieving quality: 3 test questions top-1 all hits (similarity 0.66 / 0.77 / 0.73)
- API cost: less than 1 gross (approximately 0.02 ~ 0.05 yuan, caliber: estimated at cache rate of outstanding hits)
next step
-v1: Recall quality optimization - mixed search (keyword + vector), Rerank, plus (Recall@k, Faithfulness)
-v2: Multimodular - Image Index + VLM Q & Answer
-v3: Serviceization - FastAPI + Docker + Tracking
End
Code at GitHub:github.com/huruxia/vis... [DEBT.md]: github.com/huruxia/vis...
Thanks for watching. Any questions about communication are welcome, especially with regard to segmented strategies and search assessments -- the most interesting part of RAG.
Auto-fetched and translated by AI Atlas. All rights belong to the original author.