免费AI栈:详细解析带代码示例

本文有一个短视频:YouTubeTikTok

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系统,而不需要信用卡。真正的障碍不在于金钱,而在于你是否清楚了解如何组合每一层,了解免费层的限制,以及知道何时需要切换到付费基础设施以确保质量。从这个栈开始学习和验证想法,然后当产品真正需要扩展时逐层升级。

llmragollamalanggraphmcp