免费AI栈:详细解析带代码示例
1. 总体概览
很多人认为构建一个AI系统——agent、RAG、内部chatbot——每月要花费几千美元,因为需要为OpenAI或Anthropic的API付费。实际上并非如此:有一个包含8层的栈,每一层都有免费版本或自托管选项,足以让你构建并运行一个完整的AI系统,零成本。本文逐层讲解这个栈的每一层,说明其作用,并附带具体代码示例,让你理解如何将它们组合在一起。读完本文后,你将了解为每一层选择哪种工具,以及“免费”的真实限制在哪里。
2. 为什么“零成本”并不意味着永久免费
在深入细节之前,需要澄清一点:“零成本”这里指的是软件/许可证成本,而不是无限基础设施成本。Vercel免费层有带宽限制,Supabase免费层有存储限制,本地运行Ollama消耗你的CPU/RAM。这个栈适合用于原型、学习、为小客户演示,而不是为数百万用户扩展。
注意: 如果你的实际流量超过免费层限制,你仍然需要付费——只是为基础设施(计算、存储)付费,而不是按token为LLM供应商付费。这是一个重要的区别,需要在承诺使用这个栈进行真正的生产前理解清楚。
3. 前端层:Next.js或Streamlit
前端层不需要复杂。如果你想要漂亮的UI,可以使用Next.js部署到Vercel免费层。如果只需要快速演示AI管道,Streamlit是一个更快的选择,因为不需要单独编写前端。
# app.py
import streamlit as st
import requests
st.title("内部文档问答助手")
question = st.text_input("输入你的问题")
if st.button("提问") and question:
response = requests.post(
"http://localhost:8000/ask",
json={"question": question},
timeout=30,
)
st.write(response.json()["answer"])
这段代码创建一个输入框和一个按钮。当你点击按钮时,它调用后端API(本地运行)并显示答案。运行streamlit run app.py,Streamlit会自动在localhost:8501上托管。不需要编写React,不需要构建流程——适合在投资Next.js之前验证想法的阶段。
4. 应用编排层:LangGraph
当你的系统不仅仅是’调用LLM一次然后返回结果’,而是需要多个步骤(检索→合成→验证→回答)时,你需要一个orchestrator。LangGraph将agent建模为带有状态的图,允许分支和循环。
<!-- pom.xml在这里不适用,因为LangGraph是一个Python库 -->
pip install langgraph langchain-ollama
from langgraph.graph import StateGraph, END
from typing import TypedDict
class AgentState(TypedDict):
question: str
answer: str
def retrieve_step(state: AgentState) -> AgentState:
state["answer"] = f"正在查找文档:{state['question']}"
return state
def generate_step(state: AgentState) -> AgentState:
state["answer"] += " -> 已综合生成答案"
return state
graph = StateGraph(AgentState)
graph.add_node("retrieve", retrieve_step)
graph.add_node("generate", generate_step)
graph.set_entry_point("retrieve")
graph.add_edge("retrieve", "generate")
graph.add_edge("generate", END)
app = graph.compile()
result = app.invoke({"question": "保修政策是什么?", "answer": ""})
print(result["answer"])
正在查找文档:保修政策是什么? -> 已综合生成答案
StateGraph定义了一个包含两个节点的处理流:retrieve和generate,每个节点接收state并返回新的state。set_entry_point指定node开始,add_edge连接各个步骤。当调用app.invoke(...)时,LangGraph依次遍历图并返回最终state。一切都在本地运行,不调用任何付费API。
5. RAG管道层:Chroma和Qdrant
RAG(检索增强生成)需要一个向量存储来保存embedding并按相似度进行搜索。Chroma是嵌入式的,不需要单独的服务器——适合原型。
pip install chromadb
import chromadb
client = chromadb.PersistentClient(path="./chroma_data")
collection = client.get_or_create_collection(name="policies")
collection.add(
documents=["产品从购买之日起保修12个月。",
"如果包装完好,7天内可以退换。"],
ids=["doc1", "doc2"],
)
results = collection.query(query_texts=["保修政策"], n_results=1)
print(results["documents"])
[['产品从购买之日起保修12个月。']]
PersistentClient将数据保存到磁盘而不仅在RAM中,因此数据在重新启动后仍然存在。collection.add自动为文档计算embedding(Chroma有默认的embedding函数)。collection.query查找与问题在向量上最相似的文档。如果数据集更大,你可以改用Qdrant在本地通过Docker运行——API相似但对中等规模的扩展更优化。
6. LLM层:Ollama运行本地模型
这是帮助你摆脱API账单的核心层。Ollama允许你在机器上直接运行Llama、Mistral、Gemma,没有请求数限制。
ollama pull llama3.1
ollama run llama3.1 "用一句话总结保修政策。"
产品从购买之日起保修12个月。
ollama pull命令将模型下载到你的机器上(模型经过量化以在常见的CPU/GPU上运行)。ollama run命令启动模型并发送提示,直接在终端返回结果。Ollama还在localhost:11434开放了一个REST API,所以你可以集成到Python或Java代码中。
如果后端是Spring Boot,通过RestClient调用Ollama非常简单:
import org.springframework.web.client.RestClient;
public class OllamaClient {
private final RestClient restClient = RestClient.create("http://localhost:11434");
public String generate(String model, String prompt) {
var response = restClient.post()
.uri("/api/generate")
.body(new OllamaRequest(model, prompt, false))
.retrieve()
.body(OllamaResponse.class);
return response.response();
}
record OllamaRequest(String model, String prompt, boolean stream) {}
record OllamaResponse(String response) {}
}
OllamaClient封装了对Ollama的HTTP调用。Spring 6的RestClient替代了旧的RestTemplate,写法更简洁。OllamaRequest设置stream = false以一次性接收完整响应,而不是逐个token流式传输——对于演示示例更容易处理。
7. 工具使用:MCP(模型上下文协议)
Agent只与LLM交谈是做不了任何实际事情的。MCP是一个通信标准,帮助agent调用外部工具——读取文件、查询数据库、调用GitHub API——通过统一的接口,不需要为每个工具单独编写集成。
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data/docs"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"]
}
}
}
这个配置文件声明了两个MCP服务器:一个用于文件系统(在/data/docs中读写文件),一个用于GitHub(创建issue、读取PR)。你的agent(LangGraph、Claude Code或任何支持MCP的客户端)只需连接到这些服务器并按照标准schema调用工具,不需要为每个服务单独编写API wrapper。
注意: 通过npx在本地运行的MCP服务器不会产生许可证费用,但某些服务器需要访问令牌(例如GitHub令牌)——这些令牌仍然需要从原始服务获取。
8. 代码Agent:Aider
当你想要agent自动编写和修改代码库中的代码时,Aider是一个开源CLI工具,与通过Ollama的本地模型结合效果很好。
pip install aider-chat
aider --model ollama/llama3.1 --file src/main/java/OrderService.java
上面的命令打开Aider,指定使用通过Ollama的llama3.1模型(不通过付费API),并预加载OrderService.java文件到上下文中。从这里你可以用越南语或英语输入请求(“添加负数数量验证”),Aider将生成diff并在你同意时自动提交。
9. 数据层:SQLite、DuckDB、Supabase
并非每个AI系统都需要Postgres集群。SQLite足以用于小型结构化存储,DuckDB强大于文件格式的数据分析(CSV、Parquet),无需服务器。
import duckdb
con = duckdb.connect()
result = con.execute("""
SELECT category, COUNT(*) AS total
FROM read_csv_auto('orders.csv')
GROUP BY category
ORDER BY total DESC
""").fetchall()
print(result)
[('electronics', 120), ('books', 45), ('clothing', 30)]
DuckDB使用read_csv_auto直接读取CSV文件,不需要ETL或导入到单独的数据库。SQL查询在数百万行数据上运行几乎是即时的——足以满足大多数内部分析用例。当你需要一个具有REST API和内置身份验证的后端时,Supabase免费层提供托管Postgres以及实时和存储功能。
10. 部署:Docker、Cloudflare Workers、Hugging Face
使用Docker容器化帮助你在开发机和免费主机之间进行一致的部署。
# docker-compose.yml
services:
ollama:
image: ollama/ollama
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
app:
build: .
ports:
- "8000:8000"
depends_on:
- ollama
volumes:
ollama_data:
这个文件定义了两个服务:ollama运行LLM模型,app是你的后端,依赖ollama进行推理调用。docker compose up同时启动两者,volume ollama_data在容器重启时保持已拉取的模型。当需要公开演示时,你可以将前端推送到Cloudflare Workers(边缘计算、免费层慷慨)并将小模型推送到Hugging Face Spaces,让其他人可以测试而不需要配置他们的机器。
11. 整合完整栈
实际的运行流程:用户在Streamlit上输入问题→LangGraph收到请求,调用Chroma检索相关上下文→将上下文合并到提示中,发送给Ollama→如果需要工具(读取文件、查询DB),agent通过MCP调用→结果返回到UI。一切都在一台机器或容器上运行,没有任何请求发送到付费API。
def answer_question(question: str) -> str:
docs = collection.query(query_texts=[question], n_results=2)
context = "\n".join(docs["documents"][0])
prompt = f"基于以下信息:\n{context}\n\n回答:{question}"
import ollama
response = ollama.generate(model="llama3.1", prompt=prompt)
return response["response"]
这个函数是RAG的“心脏”:从Chroma获取上下文,嵌入到提示中,调用Ollama生成最终答案。这正是最基本的retrieve-then-generate模式,如果你的用例简单,不需要复杂的框架。
12. 为数据处理部分编写测试
RAG管道中最容易测试的部分通常是在向量化前分割文本(chunking)的步骤。如果你的Java后端处理这一步,一个简单的单元测试可以帮助避免分割时的off-by-one错误。
import java.util.List;
import java.util.ArrayList;
public class DocumentChunker {
public List<String> chunk(String text, int maxLength) {
List<String> chunks = new ArrayList<>();
for (int i = 0; i < text.length(); i += maxLength) {
chunks.add(text.substring(i, Math.min(i + maxLength, text.length())));
}
return chunks;
}
}
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class DocumentChunkerTest {
@Test
void shouldSplitTextIntoFixedSizeChunks() {
DocumentChunker chunker = new DocumentChunker();
List<String> chunks = chunker.chunk("全部产品保修12个月", 10);
assertThat(chunks).hasSizeGreaterThan(1);
assertThat(chunks.get(0)).hasSize(10);
assertThat(String.join("", chunks)).isEqualTo("全部产品保修12个月");
}
}
这个测试断言三件事:长文本被分割成多个chunk,第一个chunk的大小正确,并且将所有chunk连接起来必须等于原始文本——不能丢失任何字符。这是一种隐蔽的bug,很容易在自己编写chunking逻辑但没有充分测试时发生。
13. 常见错误和何时不应该使用这个栈
- 本地模型比云API慢很多倍,如果机器没有GPU的话。在普通CPU上,一个llama3.1 8B请求可能需要5-15秒——对于演示可以接受,但对于需要即时响应的产品来说不可接受。
- 免费层有隐性速率限制。 Vercel、Supabase、Cloudflare都限制每月请求数;超过阈值应用可能被阻止,如果你不监控仪表板,你可能事先不知道。
- 嵌入式Chroma不适合多实例部署。 如果你将后端扩展到多个pod,每个pod拥有自己的Chroma文件将导致不一致——此时需要切换到Qdrant服务器或托管向量存储。
- 本地MCP服务器增加了攻击面。 文件系统MCP服务器允许agent读写文件——如果不小心暴露到互联网,这是一个严重的安全漏洞。
注意: 如果你正在构建一个具有SLA的产品,需要稳定的低延迟,或者从一开始就有大流量,这个零成本栈只应该在原型阶段使用。当真正上线时,考虑为托管LLM API或专用GPU实例付费,以确保服务质量。
14. 总结
零成本栈不是营销噱头——它是开源工具和真实免费层的组合,足以让你构建一个具有RAG、agent、tool use和完整部署的AI系统,而不需要信用卡。真正的障碍不在于金钱,而在于你是否清楚了解如何组合每一层,了解免费层的限制,以及知道何时需要切换到付费基础设施以确保质量。从这个栈开始学习和验证想法,然后当产品真正需要扩展时逐层升级。