从零搭建渐进式AI应用:Stone Soup AI工程实践指南
Stone Soup AI 是 2024 年 AI 工程实践里一个很有意思的项目代号它借了“石头汤”这个寓言的壳一锅汤开始只放一块石头和水路过的人各自加入一点食材最后煮成一锅所有人都能分享的浓汤。放到 AI 应用开发里这条思路其实非常实用——很多项目之所以卡在原型阶段不是因为模型不够强而是因为一开始就想把检索、Agent、多轮对话、权限、部署全部做完结果锅太大连水都没烧开。Stone Soup AI 的做法反过来先有一个最小的模型接口让它能跑起来、能返回结果再根据业务需要逐步加入向量检索、工具调用、上下文记忆和容器化部署。这篇文章就围绕 Stone Soup AI 的工程路径从零搭一个本地 LLM 服务再扩展成检索增强的多组件应用最后完成 Docker 部署并给出生产环境最容易踩的坑和排查链路。读完这篇文章你会掌握一套可以直接复用的 AI 应用骨架怎么封装模型调用、怎么设计配置文件、怎么把 RAG 和工具调用接进同一个请求链路、怎么避免多进程加载模型把显存打爆。适合刚开始接触 AI 应用开发、准备把模型部署成 API 服务、或者在现有项目里做 Agent 和检索增强的开发者。1. Stone Soup AI 的工程思想先煮石头再放食材1.1 石头汤原意和 AI 工程模型的映射石头汤的寓言核心不是“占便宜”而是“协作和渐进”。有人拿出一块石头有人提供水其他村民觉得反正已经有人开火了就陆续带来胡萝卜、土豆、肉和盐。最终大家分到的不只是一锅汤而是一种共同完成一件事的方法。在 AI 工程里这块“石头”可以是一个最小的模型推理服务一锅“水”是基础 HTTP API 和配置系统。这里先把对应关系列出来。寓言元素AI 工程对应物具体落点石头最小可用模型本地部署的 LLM、Embedding 模型或调用外部模型 API 的统一封装水基础工程骨架FastAPI 服务、配置读取、请求响应模型、日志村民开发者和业务方负责加入数据、评估、工具、权限、监控的人食材业务能力私有知识库、工具调用、Agent 编排、缓存、限流锅项目结构和编排层让多个组件按统一协议协作的服务层这个映射可以帮助团队先对齐一个原则第一版只解决“模型能不能稳定响应”不要急着做完整产品。当接口稳定后后续所有能力都是往这口锅里加料。1.2 为什么 2024 年这个思路重新被提起大模型本身的能力在过去两年提升很快但 AI 应用开发的复杂度并没有因此下降。单模型已经很难覆盖真实业务模型不知道企业私有数据、不能实时查库、记不住多轮上下文、也容易产生幻觉。于是 AI 工程实践里出现了 RAG、Agent、工具调用、评测体系、模型网关等一系列组件层。组件越多项目越容易失控。Stone Soup AI 这种“从最小闭环开始”的思路正好适合应对这种失控先跑通一个 API再逐步接入组件每加一个食材都要能验证它确实让汤变好喝了而不是为了架构完整而堆代码。这里要说清楚Stone Soup AI 不适合所有项目。比如已经有完整 SDK 和成熟框架支撑的企业级平台团队可以直接在框架上开发不需要从石头开始。它更适合小团队、原型验证、内部工具和需要快速看到效果的 AI 应用。适合场景不适合场景从零搭建 MVP需要快速验证模型能力已有成熟 AI 平台需要直接对接其 SDK要部署本地模型又不想引入重型框架对响应延迟有极高要求的实时通信系统团队想理解 LLM 调用、RAG、Agent 全链路所有组件都依赖外部服务不需要本地模型需要逐步替换模型、对比效果业务规则完全固定模板化即可满足1.3 初始的“石头”到底选什么Stone Soup AI 的第一版建议只包含四样东西一个可本地运行的 LLM、一个 HTTP 服务框架、一个配置文件、一个健康检查接口。模型大小根据硬件条件决定不建议第一版就追求大模型。这块石头的作用不是做到最优而是让团队尽快看到完整链路请求进来模型生成结果返回。只有这条链路通了后续加入检索、工具调用、用户管理才有意义。注意第一版不要同时接多个模型、不要写复杂 prompt 模板、不要做用户体系。Stone Soup AI 的核心是渐进式构建任何功能都必须等到最小链路稳定后再加入。2. 环境准备跑通最小项目需要哪些依赖2.1 运行环境与版本建议实际项目里不同的显卡、驱动、PyTorch 版本和模型权重会直接影响是否能跑通。这里按常见情况给出建议落地前要根据自己的硬件调整。项目推荐值说明操作系统Ubuntu 20.04 及以上生产服务器常见选择容器化部署更方便Python3.10 或 3.11新版 Transformers 对旧版本支持逐渐降低PyTorch2.1 以上根据 CUDA 版本安装对应轮子显卡驱动支持 CUDA 11.8 或 12.1使用nvidia-smi查看显存学习环境 8GB 以上生产环境建议 16GB 以上或使用量化Docker24.0 以上可选手动部署时不需要如果本机没有 GPU也可以用 CPU 跑一个小模型比如 1B 参数级别的量化模型速度慢但能验证链路。不要因为在学习环境跑不动大模型就跳过工程骨架。2.2 初始化项目结构和虚拟环境创建一个stone-soup-ai目录然后按下面结构组织代码。stone-soup-ai/ ├── app/ │ ├── __init__.py │ ├── main.py │ ├── config.yaml │ ├── services/ │ │ ├── __init__.py │ │ ├── llm.py │ │ ├── retriever.py │ │ └── orchestrator.py │ └── schemas.py ├── scripts/ │ └── check_env.py ├── data/ │ └── docs/ ├── Dockerfile └── docker-compose.yml这个目录结构把代码、配置、数据、部署文件分开。app/services里每个模块只负责一件事llm.py负责模型加载和生成retriever.py负责向量检索orchestrator.py负责把多个组件串起来。2.3 安装依赖先用虚拟环境隔离项目依赖。cd stone-soup-ai python -m venv .venv source .venv/bin/activate创建一个requirements.txt内容大致如下。fastapi0.115.5 uvicorn[standard]0.32.1 pydantic2.9.2 pyyaml6.0.2 transformers4.46.2 torch2.5.1 sentence-transformers3.1.1 faiss-cpu1.9.0.post1安装命令pip install -r requirements.txt实际项目里torch的安装方式要根据 CUDA 版本调整。如果使用 CPU 环境可以在 PyTorch 官网选择 CPU 版本安装不要直接装默认全量包否则可能白白占用几个 GB。2.4 环境检查清单在写业务代码之前先跑一个环境检查脚本确认依赖能正常导入。# scripts/check_env.py import sys def main(): print(Python:, sys.version) try: import torch print(PyTorch:, torch.__version__) print(CUDA available:, torch.cuda.is_available()) if torch.cuda.is_available(): print(GPU:, torch.cuda.get_device_name(0)) except ImportError as e: print(Missing torch:, e) try: import transformers print(Transformers:, transformers.__version__) except ImportError as e: print(Missing transformers:, e) try: import fastapi print(FastAPI:, fastapi.__version__) except ImportError as e: print(Missing fastapi:, e) if __name__ __main__: main()运行python scripts/check_env.py预期看到 Python 版本、PyTorch 版本和 Transformers 版本正常输出版号。如果CUDA available: False说明当前安装的 PyTorch 不支持 GPU后续加载模型会走 CPU。不要跳过这个检查否则模型加载阶段很难排查是代码问题还是环境问题。3. 最小闭环先让模型服务返回文字3.1 为什么第一步是做 API 服务而不是聊天页面Stone Soup AI 的第一块石头应该是“稳定的模型服务接口”而不是前端页面。原因是所有后续组件包括 RAG、Agent、日志、监控都要面对同一个服务接口。只要这个接口稳定后面替换模型、增加工具、接入前端都只是加料。这里用 FastAPI 封装一个简单的 LLM 服务让客户端通过 HTTP 请求拿到模型生成的文本。3.2 配置文件先行在app/config.yaml里写入最小配置。model: name: Qwen/Qwen2.5-1.5B-Instruct device: auto dtype: auto max_new_tokens: 512 temperature: 0.7 top_p: 0.9 repetition_penalty: 1.05 server: host: 0.0.0.0 port: 8000 max_request_length: 4096配置为什么要外置因为模型名称、设备、采样参数会随着测试调整如果写死在代码里每次改参数都要改代码。把配置独立出来业务代码不感知模型细节后面换模型时只需要改配置文件。3.3 封装模型服务在app/services/llm.py中编写模型加载和生成逻辑。import yaml from pathlib import Path from typing import List, Optional import torch from transformers import AutoModelForCausalLM, AutoTokenizer CONFIG_PATH Path(__file__).resolve().parent.parent / config.yaml class LLMService: def __init__(self, config_path: str str(CONFIG_PATH)): with open(config_path, r, encodingutf-8) as f: self.config yaml.safe_load(f) model_config self.config[model] self.tokenizer AutoTokenizer.from_pretrained( model_config[name], trust_remote_codeTrue, ) self.model AutoModelForCausalLM.from_pretrained( model_config[name], device_mapmodel_config.get(device, auto), torch_dtypemodel_config.get(dtype, auto), trust_remote_codeTrue, ) self.model.eval() def generate( self, prompt: str, history: Optional[List[dict]] None, ) - str: messages [] for item in history or []: messages.append({role: item.get(role, user), content: item.get(content, )}) messages.append({role: user, content: prompt}) text self.tokenizer.apply_chat_template( messages, tokenizeFalse, add_generation_promptTrue, ) inputs self.tokenizer(text, return_tensorspt).to(self.model.device) with torch.no_grad(): outputs self.model.generate( **inputs, max_new_tokensself.config[model].get(max_new_tokens, 512), temperatureself.config[model].get(temperature, 0.7), top_pself.config[model].get(top_p, 0.9), repetition_penaltyself.config[model].get(repetition_penalty, 1.05), do_sampleTrue, ) new_tokens outputs[0][inputs[input_ids].shape[1]:] return self.tokenizer.decode(new_tokens, skip_special_tokensTrue)关键点有三个第一apply_chat_template会根据模型对应的模板把多轮消息拼接成模型需要的格式。不同模型的 prompt 格式不一样手动拼容易出错这一步建议使用模型自带的 tokenizer。第二model.generate在torch.no_grad()下运行避免保存不必要的梯度减少显存占用。第三生成结束后只截取新增 token 对应的部分把输入 prompt 去掉返回干净结果。3.4 编写 FastAPI 入口在app/schemas.py中定义请求和响应结构。from typing import List, Optional from pydantic import BaseModel, Field class ChatMessage(BaseModel): role: str Field(defaultuser, description消息角色) content: str Field(..., description消息内容) class ChatRequest(BaseModel): prompt: str Field(..., min_length1, max_length4096) history: List[ChatMessage] Field(default_factorylist) class ChatResponse(BaseModel): reply: str prompt_tokens: Optional[int] 0 generated_tokens: Optional[int] 0在app/main.py中创建应用。from fastapi import FastAPI from app.schemas import ChatRequest, ChatResponse from app.services.llm import LLMService app FastAPI(titleStone Soup AI, version0.1.0) llm_service LLMService() app.get(/healthz) def healthz(): return {status: ok} app.post(/v1/chat, response_modelChatResponse) def chat(req: ChatRequest): reply llm_service.generate(req.prompt, history[m.model_dump() for m in req.history]) return ChatResponse(replyreply)这个接口故意设计得很小一个健康检查、一个聊天接口。后续加入检索、工具调用时也只在这个接口的请求模型里扩展字段前端和调用方不需要感知内部变化。3.5 启动和验证启动服务。uvicorn app.main:app --host 0.0.0.0 --port 8000日志里应该出现 FastAPI 的启动信息模型加载可能需要几十秒到几分钟。之后用 curl 验证。curl -X POST http://127.0.0.1:8000/v1/chat \ -H Content-Type: application/json \ -d {prompt: 用一句话解释什么是向量数据库}预期返回 JSON{ reply: 向量数据库是一种专门存储和检索高维向量的数据库常用于语义搜索和相似度匹配。, prompt_tokens: 0, generated_tokens: 0 }3.6 第一个常见坑模型加载慢和显存不足第一次加载模型时如果网络不通或 Hugging Face 仓库下载中断会长时间卡住。实际项目建议先把模型权重下载到本地目录再通过本地路径加载。如果日志里出现CUDA out of memory可以按顺序做三件事降低max_new_tokens默认 512 改成 128 或 64。使用 8 位或 4 位量化在from_pretrained中加load_in_8bitTrue或load_in_4bitTrue。换更小的模型。Stone Soup AI 的好处这时候就体现出来了石头足够小怎么煮都行。4. 往锅里加食材检索、记忆和工具调用4.1 单模型为什么不够一个只有模型生成接口的系统本质上是一个“没有记忆但会说话的函数”。它不知道业务库里的私有数据也没有持久记忆。要让汤真正有味道需要加入两种食材外部知识和可执行工具。这一节按两条线扩展检索增强把文档向量化并保存到向量库在生成前先检索相关片段。工具调用让模型可以请求执行一个函数比如查天气、查订单、调数据库接口。两条线可以同时存在Stone Soup AI 的锅要能放下它们。4.2 给汤锅加入检索基于 FAISS 的本地向量库先准备几段文档放到data/docs/下比如product.txt内容是产品说明。然后写一个retriever.py用sentence-transformers生成句子向量用 FAISS 做相似度检索。from pathlib import Path from typing import List from sentence_transformers import SentenceTransformer import faiss import numpy as np class Retriever: def __init__(self, docs_dir: str data/docs, embedding_model: str BAAI/bge-small-zh-v1.5): self.encoder SentenceTransformer(embedding_model) self.docs [] for path in sorted(Path(docs_dir).glob(*.txt)): chunks self._split_text(path.read_text(encodingutf-8), chunk_size200) self.docs.extend(chunks) if not self.docs: raise ValueError(No documents found in docs_dir) embeddings self.encoder.encode(self.docs, normalize_embeddingsTrue) dim embeddings.shape[1] self.index faiss.IndexFlatIP(dim) self.index.add(np.array(embeddings)) def _split_text(self, text: str, chunk_size: int) - List[str]: paragraphs text.split(\n) chunks [] buf for p in paragraphs: if len(buf) len(p) 1 chunk_size: if buf: chunks.append(buf.strip()) buf p else: buf \n p if buf: chunks.append(buf.strip()) return chunks def search(self, query: str, top_k: int 3) - List[str]: query_vec self.encoder.encode([query], normalize_embeddingsTrue) scores, indices self.index.search(np.array(query_vec), top_k) return [self.docs[i] for i in indices[0]]这里使用IndexFlatIP是因为 embedding 已经做了归一化内积等于余弦相似度。chunk_size200控制文档切分长度实际项目中需要根据文档结构调整过小会丢失上下文过大会浪费模型的上下文窗口并降低检索精度。4.3 加入轻量 Agent让模型能调用工具工具调用的完整链路比较复杂但最小实现可以这样理解系统给模型一个工具说明。模型生成一段 JSON表示要调用哪个函数和参数。应用解析 JSON执行函数把结果返回给模型再生成最终回答。在app/services/tools.py里注册一个最简单的查询工具。import json from typing import Callable, Dict, Any tools: Dict[str, Dict[str, Any]] {} def register_tool(name: str, description: str, parameters: dict, func: Callable): tools[name] { description: description, parameters: parameters, func: func, } def get_tool_schema(): return { name: { description: t[description], parameters: t[parameters], } for name, t in tools.items() } def execute_tool(name: str, arguments: dict) - str: if name not in tools: return json.dumps({error: ftool {name} not found}) try: result tools[name][func](**arguments) return json.dumps(result, ensure_asciiFalse) except Exception as e: return json.dumps({error: str(e)}, ensure_asciiFalse)注册一个查询方法def fake_query_balance(user_id: str): return {user_id: user_id, balance: 99.50} register_tool( namequery_balance, description查询用户余额, parameters{type: object, properties: {user_id: {type: string}}}, funcfake_query_balance, )注意这个示例只用于演示工具注册和调用思路实际接入订单、支付等系统时要严格控制权限和参数校验。4.4 把检索和工具调用织进同一请求链路在app/services/orchestrator.py里把LLMService、Retriever和工具执行器串起来。主流程是用户请求 - 检索相关文档 - 组装 system prompt - 模型生成第一轮 - 如果结果包含工具调用 JSON 则执行工具 - 拼接工具结果 - 模型生成最终回答。import json from app.services.llm import LLMService from app.services.retriever import Retriever from app.services.tools import get_tool_schema, execute_tool class Orchestrator: def __init__(self, llm: LLMService, retriever: Retriever): self.llm llm self.retriever retriever def run(self, prompt: str, historyNone): context \n.join(self.retriever.search(prompt, top_k3)) system_prompt ( 你是一个 AI 助手。请使用以下知识回答用户问题 如果知识不足以回答可以说不知道。\n\n f知识片段\n{context}\n\n 如果用户请求与工具相关请输出 JSON {tool: 工具名, arguments: {...}} ) full_prompt f{system_prompt}\n\n用户{prompt} first self.llm.generate(full_prompt, history[]) try: action json.loads(first) if isinstance(action, dict) and tool in action: tool_result execute_tool(action[tool], action.get(arguments, {})) second_prompt ( f工具执行结果{tool_result}\n f请根据结果回答用户问题{prompt} ) return self.llm.generate(second_prompt, historyhistory or []) except json.JSONDecodeError: pass return first这段代码的关键不是实现一个完整的 Agent 框架而是展示最小可用的编排逻辑。模型可能返回普通文本也可能返回工具 JSON通过json.loads尝试解析来区分。4.5 核心参数速查Stone Soup AI 扩展后会同时遇到模型参数、检索参数和工具调用超时参数。这里用一张表收拢最常见的调整项。参数位置常见值调大的影响调小的影响temperatureLLM 配置0.7回答更随机更容易发散回答更稳定可能更机械top_pLLM 配置0.9候选词更多候选词更集中可能更保守max_new_tokensLLM 配置512能生成长文本耗时增加回答可能被截断chunk_sizeRetriever 配置200上下文更完整检索精度可能下降检索更精准上下文可能不足top_kRetriever 查询3给模型更多参考片段上下文更简练可能漏关键信息请求超时代理层30s-60s长时间排队时请求不失败大模型慢生成时容易超时实际项目里这些参数不是越大越好也不是越小越好。修改时一次只改一个变量并且要留日志记录“哪一版参数产生了哪一份输出”否则很难判断效果差异来自哪里。5. 部署验证从本地调试到容器化发布5.1 为什么要容器化本地能跑通和服务器能稳定运行是两件事。模型推理服务依赖的包很多版本敏感如果每台机器手动配很容易出现本地能跑、生产环境报torch或transformers版本不一致的情况。用 Docker 可以把这个锅完整打包环境差异被隔离在容器里。以下是一个基础 Dockerfile适合 GPU 部署场景。FROM pytorch/pytorch:2.5.1-cuda12.1-cudnn9-runtime WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app ./app COPY data ./data EXPOSE 8000 CMD [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8000]5.2 用 docker-compose 管理服务和数据如果还需要单独启动向量库或者外部工具服务可以用docker-compose.yml。如果只是单服务也可以直接docker build。version: 3.9 services: stone-soup: build: . ports: - 8000:8000 volumes: - ./data:/app/data - ~/.cache/huggingface:/root/.cache/huggingface environment: - MODEL_NAMEQwen/Qwen2.5-1.5B-Instruct - MAX_NEW_TOKENS256 - TEMPERATURE0.7 deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] healthcheck: test: [CMD, curl, -f, http://localhost:8000/healthz] interval: 30s timeout: 5s retries: 3把模型权重挂到宿主机缓存目录可以避免每次重建容器都重新下载模型。健康检查使用/healthz接口这样调度平台可以自动发现服务不可用并重启容器。5.3 学习环境与生产环境差异关注点学习环境生产环境模型注册代码里写死通过环境变量或配置中心注入密钥无或临时使用 Secret 管理不落仓库日志终端输出JSON 结构化日志转到采集系统监控不需要显存、GPU 利用率、请求延迟、错误率并发单请求队列、限流、超时、自动扩缩容回滚删掉重新跑镜像版本管理保留上一可用版本数据本地文档数据源权限控制、备份、脱敏5.4 服务端的并发注意点模型加载到内存后多个请求不能直接无限制并发。常见做法有两种第一种是单进程 异步队列由 FastAPI 的请求处理函数把任务提交到队列模型单线程生成响应延迟增加但显存可控。第二种是多副本每个副本加载一份模型前端用负载均衡分发。这时要注意uvicorn的--workers参数不能随便调大否则每个 worker 都会复制一份模型显存直接翻倍。推荐在模型服务里使用全局单例确保模型只加载一次。已经实现的LLMService是在模块导入时创建的FastAPI 多 worker 下每个进程各有一份这符合“按进程隔离模型”的设计。注意模型服务不是普通 Web 服务不能像高并发接口那样堆 worker 数量。显存是硬约束正确做法是控制单副本并发数而不是盲目增加进程。5.5 发布前检查清单在把 Stone Soup AI 推上线之前按下面清单逐项检查。模型权重所在路径是否可读磁盘剩余空间是否足够。配置项是否全部通过环境变量注入默认值是否安全。/healthz是否返回正常健康检查间隔是否覆盖模型重启时间。单请求最大输入长度是否有限制超大请求是否会被拒。日志是否包含请求 ID、耗时、错误堆栈。显存是否满足最大并发场景超卖是否会导致 OOM。是否有容量预估单副本支持多少 QPS延迟 P95 是多少。镜像是否有版本号能否回滚到上一个可用版本。6. 常见问题与排查链路6.1 请求超时或返回空回复现象接口偶尔返回 200 但reply为空或者直接 504 超时。可能原因依次排查模型还在加载中请求打到了未就绪的服务。检查/healthz是否返回ok。max_new_tokens太小模型生成了空字符串。改为 256 再试。显卡显存不足生成过程报CUDA out of memory被日志丢弃。运行nvidia-smi看显存状态。多个请求同时进入模型推理产生排队导致应用层超时。检查命令nvidia-smi docker logs --tail 200 container_name curl http://127.0.0.1:8000/healthz6.2 回答质量明显偏差现象模型回答和问题无关或者复读知识片段。先从 Prompt 层排查system prompt 是否明确告诉模型“知识不足时直接说不知道”是否把检索结果原样塞进了上下文temperature是否设置得过高。再从检索层排查top_k是否太小chunk_size是否破坏了语义embedding 模型是否和业务语言匹配。中文场景使用bge-small-zh-v1.5这类中文模型比直接使用通用英文模型效果更稳定但不是所有环境都支持落地前要实测。6.3 显存持续上涨现象服务运行时间越长显存占用越高最终崩溃。常见原因是模型推理过程中缓存没有释放或者请求处理中创建了新的张量没有被及时回收。排查步骤如下检查代码里是否在每次请求时重新调用AutoModelForCausalLM.from_pretrained绝对不能这样做模型必须单例。检查torch.no_grad()是否遗漏。检查History列表是否无限增长长对话上下文可能让 prompt 长度持续增加导致推理显存自然增长。解决方案是限制历史轮数对长对话做摘要压缩在服务层控制最大并发数。6.4 Agent 工具调用报错现象模型输出的 JSON 无法解析或者工具名称不存在。让模型输出严格 JSON 本身就不稳定。最小实现里可以通过在 system prompt 中明确 JSON 示例并在解析失败时自动降级为普通文本回答。工具执行结果应作为一个独立消息重新交给模型而不是直接拼接在问题里否则模型容易分不清哪段是用户输入、哪段是工具输出。6.5 统一排查链路所有问题都建议按下面的链路排查不要一开始就怀疑模型能力。排查层级检查项常见结论输入层请求参数是否合法、prompt 是否为空、上下文长度请求格式错误代码路径是否走了预期分支、是否直接抛异常逻辑分支遗漏配置层model_name、device、max_new_tokens 是否生效配置没读或路径错误资源层GPU 显存、CPU 内存、磁盘是否充足资源不足导致 OOM日志层是否有完整异常堆栈、请求 ID 是否能串联日志缺失导致无法定位版本层torch、transformers、模型权重是否匹配版本不兼容7. 最佳实践让这一锅汤可以复制到多个项目7.1 保持组件接口稳定Stone Soup AI 的工程骨架要能复制前提是组件之间通过稳定接口通信。LLMService只暴露generate(prompt, history) - strRetriever只暴露search(query, top_k) - List[str]工具注册表只依赖一个函数签名。未来无论是换模型、换向量库还是接外部 Agent 框架都只影响对应模块内部不影响整条链路。7.2 Prompt 和上下文管理要版本化Prompt 不是临时写在代码里的字符串。建议把 system prompt 放到独立配置文件或单独目录带有版本号。变更 prompt 后要记录变化内容否则模型效果变化无法归因。7.3 从原型到生产的三个分阶段路线第一阶段只跑通模型 API 和健康检查目标是不超过一周。第二阶段加入知识检索和上下文记忆目标是在自己的业务数据上得到可接受回答。第三阶段加入 Agent 工具调用、权限校验、监控告警目标是把服务交给真实用户使用。不要试图跳阶段。Stone Soup AI 的核心是每次只加一个食材然后确认它的味道。7.4 可复用的落地清单配置独立于代码至少支持环境变量覆盖。模型实例全局唯一不在运行时重复加载。所有外部调用都有超时时间。日志携带请求 ID 和耗时。请求有最大长度限制防止超大输入拖垮服务。模型生成参数有固定默认值变更参数走配置。检索结果要限制条数避免塞爆上下文。工具调用必须有参数校验不能直接执行未经校验的 JSON。健康检查要在模型加载完成后才返回ok。每次发布保留上一个镜像版本便于回滚。Stone Soup AI 这个名字提醒开发者真正复杂的不是第一块石头而是后面不断加入的食材如何不把汤煮坏。只要最小链路是干净的后续的检索、Agent、部署和监控都可以按同一套工程方式一步步加进去。下一步你可以把这块石头换成更强的新模型也可以把小型工具注册表替换成成熟的 Agent 框架前提是保持这个骨架的接口稳定和可观测性。对刚开始接触 AI 应用开发的读者建议先照着最小 API 服务完整跑一遍再决定是否加入向量检索和工具调用顺序反过来很容易在组件调试中迷失。