系列开篇:为什么Android老兵该学AI开发?
说个真事。上个月团队技术周会,后端同学演示了一个内部知识问答系统——用RAG+Agent搭的,能对着我们几百页的业务文档回答问题,还能自动查Jira拉关联需求。当时我的第一反应是:这活儿也不难啊,不就是「数据检索→拼到Prompt里→调API」?
然后我花了一个周末试着自己搭了一个。结果发现——嗯,核心逻辑确实不难,但里面的工程细节和Android开发的相似度高得离谱。什么分层架构、缓存策略、数据源管理、异步调度……全是老朋友换了个马甲。
这就是我决定写这个系列的原因。不是教你从零学Python(你肯定会),而是用Android工程师的「母语」来翻译AI开发的核心概念,让你发现:你其实已经具备了80%的思维模型,缺的只是那20%的领域知识。
这个系列会覆盖: ① RAG(检索增强生成)—— 本篇 ② Agent智能体 —— 工具调用与任务编排 ③ 微调 —— LoRA/QLoRA实战 ④ 组合拳 —— 三者融合搭建完整AI助手 每篇都从Android的类比切入,带实战代码,保证你能跑起来。
大模型的「幻觉」问题:RecyclerView没绑数据源
你肯定遇到过这种bug:RecyclerView显示出来了,item布局也渲染了,但里面的数据全是错的——要么是空的,要么是上一次的残留。原因很简单:你忘了绑定正确的数据源。
大模型的「幻觉」问题本质上是同一件事。GPT/Claude这些模型训练数据截止到某个时间点,之后的事它不知道。你问它你们公司的内部业务逻辑,它会非常自信地瞎编——就像那个没绑数据源的RecyclerView,布局很漂亮,内容全靠蒙。
RAG(Retrieval-Augmented Generation,检索增强生成)就是解决这个问题的。核心思想极其朴素:别让模型凭记忆回答,先帮它找到相关资料,贴在问题前面一起交给它。
听起来是不是特别像你在Android里干的事?用户请求数据→先查本地缓存/数据库→拼装到UI层展示。一模一样的模式。
用ContentProvider的思维理解RAG架构
让我用一个你绝对熟悉的Android概念来翻译RAG的架构:
| RAG 概念 | Android 类比 | 干什么的 |
|---|---|---|
| 外部知识库 | ContentProvider | 存储可被检索的数据 |
| Embedding向量化 | 建索引(index) | 把数据变成可快速查询的格式 |
| Retrieval检索 | query() | 根据条件找到相关数据 |
| Prompt注入 | Cursor → 数据映射 | 把检索结果喂给消费方 |
| Generation生成 | Adapter.bind() | 基于数据生成最终输出 |
如果你把RAG系统看成一个ContentProvider的使用流程,整个架构瞬间就清晰了:
原始文档(你的知识库)
↓ 切片 + 向量化
向量数据库(你的ContentProvider)
↓ 用户提问触发 query()
检索Top-K相关文档片段
↓ 拼入Prompt(相当于bind数据)
LLM生成最终回答
RAG核心流程拆解
让我把RAG的完整流程拆开,每一步都对应到你熟悉的概念:
1. 文档切片(Chunking)—— 相当于DiffUtil拆分数据
你不会把一整个List一次性丢给RecyclerView吧?你会分页、会用DiffUtil做增量更新。RAG也一样——你不能把一份200页的PDF整个塞进Prompt(token限制),得先切成小块。
切片策略直接影响检索质量,就像分页策略影响列表加载体验。切太大——检索精度低(信噪比差);切太小——上下文丢失(用户看到半句话)。
# 文档切片 —— 类似分页逻辑
from langchain.text_splitter import (
RecursiveCharacterTextSplitter
)
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
# 重叠部分,防止切断语义
chunk_overlap=50,
separators=[
"\n\n",
"\n",
"。",
".",
" "
]
)
# 加载Android官方文档
docs = load_documents(
"android_docs/"
)
chunks = splitter.split_documents(docs)
print(
f"切成 {len(chunks)} 块"
)
那个 chunk_overlap=50 很关键——相当于RecyclerView的prefetch,让相邻块之间有重叠,避免关键信息恰好被切断在边界上。
2. 向量化(Embedding)—— 相当于给数据建索引
Room数据库为什么要加 @Index?因为你得让查询变快。Embedding做的事类似——把文本转成一个高维向量(通常是768或1536维),这样就能用「向量相似度」来做语义层面的快速检索。
关键区别是:传统索引做的是精确匹配(where id = 123),向量索引做的是语义相似度匹配("怎么做网络请求"能匹配到"Retrofit的使用方法")。这是AI检索比传统全文检索强的地方。
# Embedding —— 给文本建向量索引
from langchain_openai import (
OpenAIEmbeddings
)
from langchain_community.vectorstores import (
Chroma
)
# 选择Embedding模型
# 相当于选图片加载库
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small"
)
# 存入向量数据库
# 相当于Room.databaseBuilder()
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
3. 检索 + 生成 —— query() + bind()
用户提了一个问题,先去向量库里找最相关的K个片段(默认通常是4个),然后把这些片段和问题一起拼成Prompt交给大模型:
# 完整RAG链 = query + bind + 生成
from langchain.chains import (
RetrievalQA
)
from langchain_openai import (
ChatOpenAI
)
llm = ChatOpenAI(
model="gpt-4o",
temperature=0
)
# 构建检索器(= 配置query参数)
retriever = vectorstore.as_retriever(
search_kwargs={"k": 4}
)
# 组装RAG链
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
retriever=retriever,
return_source_documents=True
)
# 提问
result = qa_chain.invoke(
"Jetpack Compose的recomposition"
"触发条件是什么?"
)
print(result["result"])
就这么几行代码,一个能回答Android官方文档问题的RAG系统就跑起来了。是不是比想象中简单?核心就三步:切片→向量化存储→检索+生成。
向量数据库选型:SQLite vs Room vs Realm的AI版
做Android开发选数据库,你会根据场景选SQLite直接上、用Room封装、或者试试Realm。向量数据库的选型逻辑一模一样:
| 向量DB | 类比 | 适用场景 |
|---|---|---|
| Chroma | SQLite | 本地开发/原型验证,零配置即可用 |
| Pinecone | Firebase Realtime DB | 云托管,免运维,按量付费 |
| Milvus | 自建MySQL集群 | 大规模生产环境,需要自己运维 |
| Weaviate | Room(带ORM) | 内置语义搜索+混合检索,开箱即用 |
我的建议:先用Chroma跑通原型(5分钟上手),验证效果后再决定要不要迁移到Pinecone或Milvus。这和你开发App时先用内存数据验证逻辑、再接Room的思路一样。别在原型阶段就搞分布式向量集群,那是过早优化。
Embedding模型选择:Glide vs Coil的AI版
选Embedding模型和选图片加载库一样纠结。列几个主流选择给你参考:
| 模型 | 维度 | 特点 |
|---|---|---|
| OpenAI text-embedding-3-small | 1536 | 最方便,效果好,要花钱 |
| BGE-large-zh | 1024 | 中文最强开源,可本地部署 |
| Jina Embeddings v3 | 1024 | 多语言均衡,长文本支持好 |
如果你的文档主要是中文,BGE是当前性价比最高的选择——开源免费,效果在MTEB榜单上打得过大部分商用模型。类比的话,就像Coil在Kotlin项目里的地位:轻量、现代、够用。
如果图省事不想管部署,OpenAI的embedding API调用成本极低(100万token约0.02美元),相当于Glide——啥也不用想,直接用就完事了。
实战:搭建Android文档RAG问答系统
说了这么多概念,来个完整可跑的例子。目标:把Android官方文档的某个章节做成RAG,能用自然语言问答。
# pip install langchain langchain-openai
# chromadb tiktoken
import os
from langchain_community.document_loaders import (
DirectoryLoader,
TextLoader
)
from langchain.text_splitter import (
RecursiveCharacterTextSplitter
)
from langchain_openai import (
OpenAIEmbeddings,
ChatOpenAI
)
from langchain_community.vectorstores import (
Chroma
)
from langchain.chains import (
RetrievalQA
)
from langchain.prompts import (
PromptTemplate
)
# Step 1: 加载文档
loader = DirectoryLoader(
"./android_docs",
glob="**/*.md",
loader_cls=TextLoader
)
docs = loader.load()
# Step 2: 切片
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50
)
chunks = splitter.split_documents(docs)
# Step 3: 向量化并存入Chroma
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small"
)
db = Chroma.from_documents(
chunks,
embeddings,
persist_directory="./chroma_android"
)
# Step 4: 构建RAG问答链
PROMPT = PromptTemplate(
template="""基于以下Android文档内容
回答问题。如果文档中没有相关
信息,请说明你不确定。
文档内容:
{context}
问题:{question}
回答:""",
input_variables=[
"context",
"question"
]
)
qa = RetrievalQA.from_chain_type(
llm=ChatOpenAI(
model="gpt-4o",
temperature=0
),
retriever=db.as_retriever(
search_kwargs={"k": 4}
),
chain_type_kwargs={
"prompt": PROMPT
},
return_source_documents=True
)
# 试一下
resp = qa.invoke(
"ViewModel在配置变更时"
"如何保持数据?"
)
print(resp["result"])
print("---来源---")
for doc in resp["source_documents"]:
print(
doc.metadata["source"][:40]
)
整个代码50行左右,核心逻辑清晰得就像一个标准的Android Repository模式:DataSource→Repository→ViewModel→UI。
RAG调优三板斧
跑是跑起来了,但你会发现——检索质量时好时坏。有时候问个简单问题能检索到完美的文档片段,有时候明明文档里有答案却死活找不到。
这就像RecyclerView列表滑动卡顿——问题往往不在UI层,而在数据层。RAG的「卡顿」(回答质量差)也一样,99%的问题出在检索环节,不是大模型不行。
板斧一:优化Chunk策略
默认的固定长度切片太粗暴了。更好的做法是按语义边界切——标题、段落、代码块各自成chunk,保持语义完整性。
# Markdown专用切片器
from langchain.text_splitter import (
MarkdownHeaderTextSplitter
)
headers = [
("#", "h1"),
("##", "h2"),
("###", "h3"),
]
md_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers
)
# 每个chunk保留层级上下文
# 类似Fragment知道自己在
# 哪个Activity下
板斧二:混合检索(Hybrid Search)
纯向量检索有个致命弱点:对专有名词和精确匹配不敏感。你搜"ViewModelScope",语义检索可能给你返回"协程作用域管理"相关的内容——语义是对的,但不是你要的那个具体类。
解决方案:混合检索 = 向量检索(语义) + BM25/关键词检索(精确)。两路结果合并排序,取长补短。
# 混合检索 = 向量 + 关键词
from langchain.retrievers import (
EnsembleRetriever
)
from langchain_community.retrievers import (
BM25Retriever
)
# 关键词检索器
bm25 = BM25Retriever.from_documents(
chunks
)
bm25.k = 4
# 向量检索器
vector_retriever = db.as_retriever(
search_kwargs={"k": 4}
)
# 融合:各占50%权重
ensemble = EnsembleRetriever(
retrievers=[
bm25,
vector_retriever
],
weights=[0.5, 0.5]
)
这就像Android的搜索功能同时走本地SQLite FTS全文索引和远程搜索API,然后合并结果——各有擅长,组合最强。
板斧三:Reranker重排序
检索回来的Top-K结果排序不一定准确。Reranker是一个轻量的交叉注意力模型,专门做「这个文档片段和用户问题到底有多相关」的精细打分。
类比的话——初次检索相当于RecyclerView的粗略布局(onMeasure),Reranker相当于精确布局(onLayout)。先粗筛,再精排。
# Reranker重排序
# pip install flashrank
from langchain.retrievers import (
ContextualCompressionRetriever
)
from langchain_community.document_compressors import (
FlashrankRerank
)
# 先检索20个候选
base_retriever = db.as_retriever(
search_kwargs={"k": 20}
)
# Reranker精排到Top-4
compressor = FlashrankRerank(
top_n=4
)
rerank_retriever = (
ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=base_retriever
)
)
常见踩坑:那些让你白debug半天的问题
做了两个月RAG项目,分享几个血泪教训:
坑1:chunk太大导致检索命中但答案被淹没
chunk设成2000字符,检索确实命中了相关文档,但那个关键信息只占其中一小段。大模型面对一大堆上下文,反而找不到重点。解决:chunk_size控制在300-600,宁可多检索几个chunk。
坑2:Embedding模型和查询语言不匹配
用英文Embedding模型存中文文档,检索质量断崖下跌。就像在英文键盘上打中文——能用,但体验极差。中文文档一定要用中文优化过的Embedding模型(BGE-zh/M3E)。
坑3:没做Query改写
用户问"怎么让列表不卡",你的文档里写的是"RecyclerView性能优化"。直接拿用户原话去做向量检索,可能匹配不上。做一步Query Rewrite(让LLM先把用户问题改写成更精确的检索query),召回率能提升30%+。
坑4:忽略了metadata过滤
文档有版本、有时间。用户问Compose的问题,你把Compose 1.0和最新版的文档混在一起返回——信息冲突,大模型也懵了。给chunk打上metadata标签(版本号、更新时间、所属模块),检索时加filter。
一句话总结RAG调优思路:检索质量差≈列表滑动卡顿。问题99%出在数据层(切片策略/Embedding质量/检索逻辑),不是出在LLM生成层。先优化「数据供给」,再考虑换更贵的模型。
写在最后:你的Android经验比你想象的值钱
回顾一下今天的内容——RAG的核心就是「先检索,再生成」。而这种「数据驱动UI」的思维模式,Android工程师每天都在用。你做过的Repository模式、做过的缓存策略、做过的搜索功能优化,全都能直接迁移到RAG系统的设计中。
AI开发不是另一个世界。它是你已有能力的延伸,不是推倒重来。
下一篇,我们聊Agent智能体——让AI不只是回答问题,还能自己调API干活。如果说RAG是"给AI装了个本地数据库",那Agent就是"给AI装了一套Intent系统"。到时候你会发现,Android的Service/BroadcastReceiver/WorkManager这些老朋友,又要派上用场了。
—— 觉得有用?转发给你身边想跨界AI的Android同行 ——