AI 创业的技术选型——用最小技术栈快速验证
AI 创业的技术选型——用最小技术栈快速验证
适读人群:想做 AI 应用创业的工程师 | 阅读时长:约12分钟 | 核心价值:避免早期技术过度投入,用最简单的技术栈快速验证你的想法
我见过一个朋友,做了四个月,写了两万行代码,把架构搞得和大厂一样复杂——微服务、消息队列、向量数据库集群、CI/CD 流水线——然后发现没有人要用他做的东西。
四个月的技术投入打水漂了。
我也见过另一种情况:一个产品经理背景的同学,用了三周,搭了一个外表简陋但核心逻辑跑通的 AI 工具,找到了20个付费用户,开始验证产品方向。
两种人,四个月后的处境完全不同。
这篇文章想说的是:创业早期,技术是服务于验证的,而不是追求完美的。
创业早期最大的风险不是技术
很多工程师创业,倾向于把技术做得"正确"。觉得架构设计好了,后面才能扩展;代码写得干净,以后才好维护;基础设施搭好,以后才能支撑用户增长。
这些想法没错,但时机错了。
创业早期最大的风险不是技术风险,而是方向风险:你做的东西有没有人要?用户愿不愿意付钱?解决的是不是真实的痛点?
方向对了,技术问题可以后来修。方向错了,技术再好也没用。
所以早期的技术选型原则只有一个:最快速度跑通核心价值,让真实用户来验证方向。
一个过度设计的真实案例
我见过的一个案例(稍微改了细节保护当事人):
一个 Java 工程师要做"AI 合同审核"产品。他的技术方案:
微服务架构:
- 用户服务(Spring Boot)
- 合同上传服务(Spring Boot)
- AI 分析服务(Python FastAPI)
- 通知服务(Spring Boot)
- API 网关(Spring Cloud Gateway)
- 消息队列(Kafka)
- 向量数据库(Milvus 集群)
- 关系数据库(PostgreSQL 主从)
- 对象存储(MinIO)
- 监控(Prometheus + Grafana)
- CI/CD(Jenkins)搭这套东西花了两个月。到他真正开始做 AI 分析核心功能的时候,已经精力耗尽,剩下两个月随便做了做就上线了。
结果是:用户反馈 AI 审核的准确率不行,找不到愿意付费的用户。
他那套基础设施完全没派上用场,因为没有到需要扩展的那一步。
最小技术栈:够用就好
我推荐的早期技术栈是:
前端/交互:
- Streamlit 或 Gradio(如果用户是技术人员)
- 微信小程序 + 云开发(如果面向普通用户)
- 飞书/钉钉机器人(如果面向企业用户,接入成本最低)
后端:
- 单体 FastAPI 应用(一个服务搞定所有接口)
- SQLite 或单个 PostgreSQL 实例(别上集群)
AI 核心:
- 直接用 OpenAI/Anthropic/通义 API(别自部署)
- 向量检索用 Chroma(本地,零运维)或 Qdrant 单机版
部署:
- 单台云服务器(2核4G 够了,不够再升)
- Docker Compose 部署(比 Kubernetes 简单100倍)
- GitHub Actions 做 CI(够用)就这些。不用微服务,不用 Kafka,不用集群。
具体怎么搭:一个最小 RAG 产品的代码
以"企业知识库问答"为例,展示最小实现:
# main.py - 整个后端就这一个文件(早期完全够用)
from fastapi import FastAPI, UploadFile, HTTPException
from fastapi.staticfiles import StaticFiles
import chromadb
from anthropic import Anthropic
import pdfplumber
import uuid
app = FastAPI()
client = Anthropic()
chroma = chromadb.PersistentClient(path="./data/chroma") # 本地存储,零运维
# 确保 collection 存在
try:
kb = chroma.get_collection("knowledge_base")
except:
kb = chroma.create_collection("knowledge_base")
def split_text(text: str, chunk_size: int = 500) -> list[str]:
"""简单的文本分块,够用就行"""
sentences = text.replace('\n', ' ').split('。')
chunks = []
current = ""
for s in sentences:
if len(current) + len(s) < chunk_size:
current += s + "。"
else:
if current:
chunks.append(current.strip())
current = s + "。"
if current.strip():
chunks.append(current.strip())
return chunks
@app.post("/api/upload")
async def upload_document(file: UploadFile):
"""上传文档接口"""
if not file.filename.endswith('.pdf'):
raise HTTPException(400, "目前只支持 PDF 文件")
# 提取文本
content = await file.read()
with pdfplumber.open(io.BytesIO(content)) as pdf:
text = "\n".join([p.extract_text() or "" for p in pdf.pages])
if not text.strip():
raise HTTPException(400, "文档内容为空或无法提取文字")
# 分块入库
chunks = split_text(text)
doc_id = str(uuid.uuid4())
kb.add(
documents=chunks,
ids=[f"{doc_id}_{i}" for i in range(len(chunks))],
metadatas=[{"filename": file.filename, "doc_id": doc_id} for _ in chunks]
)
return {
"doc_id": doc_id,
"filename": file.filename,
"chunks": len(chunks),
"message": "上传成功"
}
@app.post("/api/ask")
async def ask_question(body: dict):
"""提问接口"""
question = body.get("question", "").strip()
if not question:
raise HTTPException(400, "问题不能为空")
# 检索相关内容
results = kb.query(
query_texts=[question],
n_results=5
)
if not results['documents'][0]:
return {"answer": "知识库暂时没有相关内容,请上传相关文档后再试。"}
context = "\n\n".join(results['documents'][0])
# 调用 AI
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=[{
"role": "user",
"content": f"""请根据以下资料回答问题:
{context}
问题:{question}
要求:只根据资料内容回答,资料中没有的信息请说"资料中未找到相关信息"。"""
}]
)
return {
"answer": response.content[0].text,
"sources": list(set([m['filename'] for m in results['metadatas'][0]]))
}
# 前端静态文件(一个简单的 HTML 页面就够了)
app.mount("/", StaticFiles(directory="static", html=True), name="static")前端用一个简单的 HTML:
<!-- static/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>企业知识库</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: 50px auto; padding: 0 20px; }
.chat-box { border: 1px solid #ddd; border-radius: 8px; padding: 20px; min-height: 300px; margin: 20px 0; }
.message { margin: 10px 0; padding: 10px; border-radius: 6px; }
.user { background: #e3f2fd; text-align: right; }
.assistant { background: #f5f5f5; }
input, button { padding: 10px; font-size: 14px; }
input { width: 70%; border: 1px solid #ddd; border-radius: 4px; }
button { background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<h1>企业知识库问答</h1>
<div>
<input type="file" id="fileInput" accept=".pdf">
<button onclick="uploadFile()">上传文档</button>
<span id="uploadStatus"></span>
</div>
<div class="chat-box" id="chatBox"></div>
<div>
<input type="text" id="questionInput" placeholder="输入你的问题..."
onkeypress="if(event.key==='Enter') askQuestion()">
<button onclick="askQuestion()">提问</button>
</div>
<script>
async function uploadFile() {
const file = document.getElementById('fileInput').files[0];
if (!file) return alert('请选择文件');
const form = new FormData();
form.append('file', file);
document.getElementById('uploadStatus').textContent = '上传中...';
const res = await fetch('/api/upload', { method: 'POST', body: form });
const data = await res.json();
document.getElementById('uploadStatus').textContent =
res.ok ? `✓ ${data.filename} 上传成功(${data.chunks} 个片段)` : `✗ ${data.detail}`;
}
async function askQuestion() {
const input = document.getElementById('questionInput');
const question = input.value.trim();
if (!question) return;
appendMessage(question, 'user');
input.value = '';
const res = await fetch('/api/ask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question })
});
const data = await res.json();
appendMessage(data.answer, 'assistant');
}
function appendMessage(text, role) {
const box = document.getElementById('chatBox');
const div = document.createElement('div');
div.className = `message ${role}`;
div.textContent = text;
box.appendChild(div);
box.scrollTop = box.scrollHeight;
}
</script>
</body>
</html>部署:
# docker-compose.yml - 就这么简单
version: '3.8'
services:
app:
build: .
ports:
- "80:8000"
volumes:
- ./data:/app/data # 持久化 Chroma 数据
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
restart: always这套东西从零到跑起来,一个工程师两三天可以搞定,不包括需求分析和内容准备。
什么时候才需要升级技术栈
最小技术栈的适用范围:
- 用户量低于1000
- 并发低于每秒10个请求
- 团队只有1-2个人
- 产品方向还没有验证
到了什么程度才需要升级?
从 SQLite 升级到 PostgreSQL:当单文件数据库在并发写入时开始出现锁等待,或者文件超过了 1GB。
从 Chroma 本地存到 Qdrant 或 Pinecone:当知识库文档超过几万个 chunks,或者需要水平扩展。
从单实例升级到多实例:当 CPU 持续超过 70%,或者需要零停机部署。
从单体到服务拆分:当不同模块的部署频率差异很大,或者不同模块的资源需求差异很大,或者团队扩大到需要独立开发不同模块。
这些触发条件出现时,再去升级,那时候你有真实的问题要解决,升级的方向是清晰的。
验证阶段最重要的几个指标
早期技术投入最终要服务于验证。验证什么?
不是验证技术能不能跑,而是验证:
- 有没有人在用:日活、周活是不是在增长
- 有没有人付钱:哪怕是10个人付了1块钱,也比1000个人白用更有价值
- 核心功能的满意度:用户对 AI 回答质量的评分,不是系统稳定性
技术能不能支撑这三个指标的测量,是判断技术投入够不够的标准,不是系统有没有达到"工程最佳实践"。
我见过太多工程师,把大量时间花在让系统达到"生产级别",却没有一个真实用户在用。没有用户的生产级别系统,是最贵的玩具。
如果你现在正在做 AI 应用创业,或者想做,花10%的精力搭一个最小可用的技术栈,剩下90%的时间找用户、做产品。等用户来了,有了付费,技术问题自然值得花时间解决。
