
当大模型能力趋于同质化,应用层的定制化开发成为竞争主战场。本文结合“Vibe Coding”理念,以构建一个企业级文档智能问答系统为例,完整展示从需求分析、技术选型、RAG 实现、Prompt 优化到生产部署的全链路实践,并附可运行代码与性能调优策略。
2026 年,基础大模型(GPT-5、Claude 4、Gemini Ultra 等)的 API 调用成本已降至每百万 token 不足 0.5 美元,模型本身的“智力”差距正在缩小。真正的业务价值来自 定制化 —— 让模型理解领域术语、遵循企业流程、接入私有数据,并以稳定、低延迟的形态交付给终端用户。
与此同时,Vibe Coding(由 Andrej Karpathy 提出的概念)强调“开发者与 AI 结对编程”,利用 AI 辅助生成样板代码、测试用例和文档,让人类开发者聚焦于核心架构与业务逻辑。这种开发范式天然适合 AI 应用定制化——因为 AI 应用本身就是在“教模型做事”,而 Vibe Coding 则让我们用“教 AI 写代码”的方式来加速这一过程。
本文将以一个企业级文档智能问答系统为实战项目,深度拆解定制化开发中的关键环节:
所有代码均已开源(文末附链接),并针对思否读者注明了关键技术难点和踩坑记录。
层级 | 组件 | 选型理由 |
|---|---|---|
大模型 | 混元 / GLM-4 / Qwen-72B(可插拔) | 支持私有化部署,中文理解优秀,且提供兼容 OpenAI 的 API 接口 |
Embedding | BGE-M3(开源) | 多语言、多粒度,支持稠密+稀疏混合检索 |
向量数据库 | Milvus 2.4(分布式) | 支持 GPU 索引(IVF-PQ),亿级向量毫秒查询 |
文档解析 | Unstructured.io + PyMuPDF + Tesseract | 处理扫描件、表格、复杂排版 |
分块策略 | 语义分块(LangChain SemanticChunker)+ 重叠滑动窗口 | 保证语义完整性,避免关键信息截断 |
编排框架 | LangGraph(替代传统 Chain) | 支持循环、条件分支和状态管理,适合复杂对话流 |
缓存 | Redis(语义缓存 + 精确缓存) | 对常见问题命中缓存,降低延迟和成本 |
监控 | LangSmith + Prometheus + Grafana | 追踪 trace、评估回答质量 |
text
用户请求 → FastAPI Gateway → 对话管理器(意图识别)→
检索器(向量检索 + BM25 关键词检索 → RRF 融合)→
重排序(Cross-Encoder)→ 上下文构建 → LLM 生成 →
后处理(安全过滤+格式校验)→ 返回 + 异步缓存写入这是定制化中最容易出坑的环节。我们采用 Unstructured 进行格式归一化,然后使用 语义分块 而非固定长度分块。
# chunking.py
from langchain_experimental.text_splitter import SemanticChunker
from langchain_community.embeddings import HuggingFaceEmbeddings
embedder = HuggingFaceEmbeddings(
model_name="BAAI/bge-m3",
model_kwargs={"device": "cuda"},
encode_kwargs={"normalize_embeddings": True}
)
# 语义分块器:基于余弦距离的断点检测
splitter = SemanticChunker(
embeddings=embedder,
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=0.85, # 相似度低于此阈值则切分
min_chunk_size=200,
max_chunk_size=800
)
# 处理 PDF 示例
from unstructured.partition.pdf import partition_pdf
elements = partition_pdf("report.pdf", strategy="hi_res", extract_images_in_pdf=True)
text = "\n".join([el.text for el in elements if el.category not in ["Image", "Table"]])
chunks = splitter.split_text(text) # 返回 List[str]踩坑记录:PDF 中的表格会被 Unstructured 转为 HTML 或 Markdown,丢失行列关系。我们额外集成了 pymupdf 的 get_text("dict") 提取原始坐标,再用 pandas 重建表格结构,确保表格数据可被向量化。
纯向量检索容易丢失关键词匹配(如产品型号、合同编号),因此我们实现 稠密+稀疏双路召回,并用 RRF(倒数排名融合) 合并结果,再经过一个轻量级 Cross-Encoder 精排。
# retriever.py
from pymilvus import Collection, AnnSearchRequest, KeywordSearchRequest
from pymilvus import RRFRanker
def hybrid_search(query: str, top_k=20):
# 1. 生成向量和稀疏向量(BM25)
dense_emb = embedder.embed_query(query)
sparse_emb = bm25_encoder.encode_queries(query) # 使用 bm25s 库
# 2. Milvus 混合检索
dense_req = AnnSearchRequest([dense_emb], "dense_vector", param={"metric_type": "IP", "params": {"nprobe": 16}}, limit=top_k)
sparse_req = KeywordSearchRequest([sparse_emb], "sparse_vector", param={"metric_type": "IP"}, limit=top_k)
res = collection.hybrid_search(reqs=[dense_req, sparse_req], ranker=RRFRanker(), limit=top_k, output_fields=["text", "metadata"])
return res[0] # 返回前十
# 3. 重排序(Cross-Encoder)
from sentence_transformers import CrossEncoder
reranker = CrossEncoder('BAAI/bge-reranker-v2-m3') # 多语言重排
pairs = [[query, hit['text']] for hit in results]
scores = reranker.predict(pairs)
final = sorted(zip(results, scores), key=lambda x: x[1], reverse=True)[:5]为什么不用 ColBERT? 对于企业级私有部署,ColBERT 的延迟过高(需二次打分),而 BGE 重排器在 GPU 上 batch 推理可做到 50ms/100 条,效果接近 ColBERTv2。
多轮对话需要记忆历史,同时要区分“追问”、“澄清”和“新话题”。我们使用 LangGraph 构建状态机:
# graph.py
from langgraph.graph import StateGraph, END
from typing import TypedDict, List
class AgentState(TypedDict):
messages: List[dict] # 对话历史
query: str
context: str
intent: str
answer: str
def intent_node(state: AgentState):
# 使用轻量分类器(或小模型)判断意图
prompt = f"判断用户意图:{state['query']}。选项:[general, follow-up, clarification, new_topic]"
# 调用 LLM(可用 GPT-3.5-turbo)获得 intent
return {"intent": "follow-up"}
def retrieve_node(state: AgentState):
if state['intent'] == 'general':
# 走常规检索
docs = hybrid_search(state['query'])
elif state['intent'] == 'follow-up':
# 结合上一轮上下文改写查询
new_q = rewrite_query(state['query'], state['messages'][-2:])
docs = hybrid_search(new_q)
# ...
return {"context": format_docs(docs)}
def generate_node(state: AgentState):
sys_prompt = build_system_prompt(state['context'], state['intent'])
resp = llm.invoke([{"role": "system", "content": sys_prompt}] + state['messages'])
return {"answer": resp}
builder = StateGraph(AgentState)
builder.add_node("intent", intent_node)
builder.add_node("retrieve", retrieve_node)
builder.add_node("generate", generate_node)
builder.set_entry_point("intent")
builder.add_edge("intent", "retrieve")
builder.add_edge("retrieve", "generate")
builder.add_edge("generate", END)
app = builder.compile()Vibe Coding 实践:上述 StateGraph 的样板代码由 Cursor AI 辅助生成,我们只需定义状态字段和节点函数签名,AI 自动补全了 add_edge 和条件分支逻辑,极大加速了原型搭建。
我们采用 结构化 Prompt(以 YAML 头信息描述角色、风格、约束),而非纯自然语言指令,便于版本管理和自动化测试。
# system_prompt.yaml
role: "企业财务咨询助手"
style: "专业、严谨,引用来源时注明文档名称和页码"
constraints:
- "若检索内容不足,直接告知无法回答,禁止编造数据"
- "回答必须分点,每点不超过50字"
- "涉及金额时,保留两位小数并添加人民币符号"在生成时动态加载:
import yaml
with open("system_prompt.yaml") as f:
prompt_config = yaml.safe_load(f)
sys_msg = f"你是{prompt_config['role']},风格:{prompt_config['style']}。约束:{', '.join(prompt_config['constraints'])}。\n\n参考材料:\n{context}"进阶技巧:我们使用 自动 Prompt 优化(如 DSPy)对指令进行贝叶斯搜索,在线下评估集上提升答案相关性 F1 从 0.72 到 0.81。
为降低 API 调用成本,我们实现 两层缓存:
SETEX):对完全相同的查询直接返回历史答案vector-similarity 阈值):对相似度 >0.95 的新查询复用缓存,需谨慎设计,避免误匹配。import redis
import numpy as np
r = redis.Redis(host='localhost', decode_responses=True)
def cached_generate(query: str):
# 精确匹配
if r.exists(f"exact:{query}"):
return r.get(f"exact:{query}")
# 语义匹配(遍历缓存键的 embedding,可用 Faiss 加速)
# 省略实现细节...
# 未命中则调用 LLM
ans = llm.invoke(query)
r.setex(f"exact:{query}", 3600, ans)
return ans生产环境中,我们使用 redisearch 模块支持向量索引,实现毫秒级语义缓存查询。
# Dockerfile
FROM python:3.11-slim
RUN pip install poetry && poetry config virtualenvs.create false
COPY pyproject.toml poetry.lock .
RUN poetry install --no-dev
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]K8s 部署要点:
维度 | 指标 | 工具 |
|---|---|---|
延迟 | P50/P95/P99 响应时间 | Prometheus + FastAPI 中间件 |
召回质量 | Recall@10, MRR | 离线评估脚本(定期运行) |
生成质量 | 忠实度(Faithfulness)、相关性(Answer Relevance) | RAGAS 评估框架 |
成本 | 每请求 token 数 + 缓存命中率 | 自定义埋点 |
我们在 LangSmith 中记录每次生成的 trace_id,并与业务日志关联,方便排查特定用户的 bad case。
很多开发者纠结于此。我们的经验规则:
对于本系统,我们完全采用 RAG + 动态 Prompt,并通过 上下文注入 实现“伪微调”效果——例如在系统提示中加入 5 个 Few-shot 示例,足以应对大多数定制化需求。
├── app/
│ ├── api/ # FastAPI 路由
│ ├── core/ # 配置、常量
│ ├── retrieval/ # 向量索引、混合检索
│ ├── graph/ # LangGraph 工作流
│ ├── prompt/ # YAML 模板 + 优化器
│ └── utils/ # 缓存、日志
├── scripts/ # 离线索引构建、评估
├── tests/ # 单元测试(pytest)
├── deploy/ # K8s yaml + Dockerfile
└── pyproject.toml启动开发环境:
poetry install
export MILVUS_HOST=localhost REDIS_URL=redis://localhost
python scripts/build_index.py --data_dir ./docs
uvicorn app.main:app --reload问题 | 表现 | 解决方案 |
|---|---|---|
分块切断关键实体 | “合同编号 ABC-123” 被拆到两段 | 使用 SemanticChunker 并设置 min_chunk_size,且添加正则保护(如数字+连字符不切割) |
多轮对话上下文爆炸 | 历史消息过长,超出 8k context | 使用滑动窗口保留最近 3 轮,并利用 LLM 对历史做摘要压缩 |
向量检索返回低相关结果 | 用户问“去年营收”但召回“前年” | 增加时间过滤(metadata 中的年份字段),在 Milvus 中使用 expr 进行标量过滤 |
GPU 显存不足 | 同时加载 Embedding + 重排器 + LLM | 将 Embedding 和重排器部署在 CPU(使用 OpenVINO 加速),LLM 独占 GPU |
本文通过一个完整的定制化 AI 应用案例,展示了从“Vibe Coding”快速原型到生产级部署的全流程。关键收获:
下一步,我们将探索 Agentic RAG —— 让系统自主决定何时检索、何时使用工具(如计算器、SQL 查询),并引入 多模态理解(图表解析),敬请期待。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。