你大概听过“石头汤”的故事:几个穷困的旅人走到一个村庄,架起一口大锅,放一块石头进去煮水,说自己在做一锅美味的石头汤。路过的村民好奇,有人送来胡萝卜,有人送来土豆,有人送来几块肉。最后,大家真的喝上了一锅丰盛的汤。
2024 年之后,很多 AI 应用开发的演进方式,和这个故事越来越像:一开始只有一个大模型 API,看起来什么也做不了;但当你把它放在一个可扩展的骨架里,数据层、工具层、记忆层、评测层像“村民们”一样不断向锅里加料,最终长成一个完整、可用的 AI 系统。这就是本文要讲的Stone Soup AI(2024)工程思路:用最小骨架启动,靠渐进式集成把 AI 能力真正组合起来。
本文将围绕这套思路展开,先讲清楚 Stone Soup AI 是什么、它解决什么问题,然后带你把一个可运行的多模型对话骨架搭出来,再逐步加入工具调用、向量检索、Agent 等能力。适合正在做 AI 应用开发、想从“调 API”走向“做系统”的读者。
1. Stone Soup AI 是什么
1.1 从寓言到工程思想
“石头汤”这个故事的核心不是那块石头,而是协作与渐进:一个不起眼的最小起点,通过持续加入模块,最终变成远超单个部件价值的结果。
AI 开发中的 Stone Soup 思想可以这样理解:
- 石头 = 大模型 API:GPT、Claude、通义、文心、DeepSeek 等,它们更像是“能说话的引擎”,不是完整产品。
- 锅 = 应用骨架:负责接收请求、维护会话、调用模型、返回结果。
- 不断加入的料 = RAG、工具调用、Agent、记忆、权限控制、评测、日志。
换句话说,Stone Soup AI 是一种面向 AI 应用的渐进式集成方法论。它强调一开始不要追求“大而全”,而是先有一个能跑通的最小闭环,再通过可插拔模块把能力逐层补齐。
1.2 它与“传统软件开发”有什么不同
传统后端开发里,数据模型、接口、权限、缓存通常在一开始就要设计好,中途替换成本很高。但 AI 应用最大的特点是不确定性:
- 模型能力在快速变化,今天用的模型明天可能被更强的替代。
- 提示词、上下文策略、工具编排没有唯一标准答案。
- 同一个任务,不同模型表现差异非常大。
Stone Soup AI 的思路是:把稳定部分做成接口和骨架,把不稳定部分做成可替换的“食材”。这样当模型升级、新的数据源接入、新的工具出现时,不需要推翻整口锅。
1.3 常见应用场景
| 场景 | 用 Stone Soup 思路解决的问题 |
|---|---|
| 企业知识库问答 | 先用大模型做对话,再逐步接入向量库、权限过滤、溯源引用 |
| 智能客服 | 先跑通对话流程,再加入工具查询订单、退换货策略、人工接管 |
| 代码助手 | 先做代码问答,再接入仓库索引、CI 状态、自动修改提交 |
| Agent 应用 | 先定义工具协议,再逐步注册更多可调用能力 |
| 内部效率工具 | 先做一个统一对话入口,再连接各类业务系统 |
这篇文章里,我们会用实际代码演示这套思路的核心骨架。
2. 核心设计理念
在动手写代码之前,先把 Stone Soup AI 的四个核心设计理念说清楚。这些理念决定了项目结构怎么写、接口怎么定,也会帮助你以后理解市面上 AI 框架的设计动机。
2.1 最小可用骨架(Minimal Skeleton)
第一版代码只需要满足三个功能:
- 接收用户消息。
- 把消息发给某个大模型。
- 把模型回复返回给用户。
不要在第一版加入登录、向量库、多租户、监控告警。这些功能如果和骨架耦合在一起,后续很容易把系统越搞越重。最小骨架的价值在于:让业务方看到一条真实可用的链路,再决定优先加什么调料。
2.2 接口先行,而非实现先行
Stone Soup AI 的核心是“可替换”。可替换的前提是抽象接口。下面这个例子是最核心的抽象:
# 模型网关接口(伪代码) class ModelGateway: def chat(self, messages, temperature=0.7, tools=None): """ 所有模型统一走这个方法。 输入统一是 OpenAI 风格的 message 列表。 输出统一是包含 role/content 或 tool_calls 的对象。 """ raise NotImplementedError不管后端接的是 OpenAI、Claude、Azure OpenAI、本地部署的模型,还是各种国内大模型,只要外面套一层协议转换,上层应用代码就不需要改动。这样你换模型的时候,动的是“食材”,不是“锅”。
2.3 渐进式功能叠加(Incremental Enhancement)
先有基础对话,再加工具;先有工具,再做 Agent 循环;先有单轮,再维护多轮记忆;先有单知识库,再做多知识源路由。每加一个能力都要保证之前的功能依然正常运行。
这也是为什么最好有一个小的回归测试集:每次加“调料”之后,跑一遍冒烟用例,确认基础对话没有被破坏。
2.4 可观测与可控(Observability and Control)
AI 系统的黑盒特性决定了日志和开关非常重要。每个请求都要能追踪到:
- 用户输入。
- 模型名称与参数。
- 上下文长度。
- 工具调用过程。
- 模型输出。
- 消耗的 token。
- 延迟。
同时要保留“开关”,比如在某个模型效果不佳时,能在配置中心一键切换。Stone Soup AI 的工程化程度,很大程度上取决于这些基础设施是否从一开始就预留好了位置。
3. 环境准备与项目结构
3.1 运行环境说明
本文的示例使用以下环境:
- Python 3.10 及以上。
- FastAPI 作为服务框架。
- openai Python SDK 1.x,用于调用兼容 OpenAI 协议的模型服务。
- uvicorn 用于启动服务。
需要注意:不同版本 SDK 在工具调用、消息对象序列化上有差异。示例代码展示的是整体工程思路,如果你的 SDK 版本或模型接口不通,优先查看对应版本的官方文档,把调用参数调整成你环境的真实写法。
3.2 安装依赖
建议先创建虚拟环境:
python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate然后安装依赖:
pip install fastapi uvicorn openai pydantic python-dotenv如果你使用的是国内模型服务,只要它提供 OpenAI 兼容接口,通常可以通过设置 base_url 来对接。
3.3 项目目录
我们建立一个 demo 项目,目录结构如下:
stone-soup-ai/ ├── .env ├── requirements.txt ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 入口 │ ├── config.py # 配置读取 │ ├── models.py # 请求/响应数据结构 │ ├── gateway.py # 模型网关 │ └── tools.py # 工具注册中心这个结构虽然简单,但已经体现了“骨架 + 可插拔模块”的分层思想。
4. 完整实战:从一锅清水开始
现在我们来写一段完整可运行的代码。先跑通一个纯对话版本,再向锅里加“工具调用”这一味关键调料。
4.1 配置管理
创建.env文件,把模型相关的配置放到这里,避免把密钥写死在代码里:
# .env LLM_API_KEY=your-api-key LLM_BASE_URL=https://api.openai.com/v1 LLM_MODEL=gpt-4o-mini如果使用国内兼容 OpenAI 协议的服务,把LLM_BASE_URL改成你的服务地址,把LLM_MODEL改成实际模型名即可。
对应的配置读取模块:
# app/config.py import os from dotenv import load_dotenv load_dotenv() class Settings: api_key: str = os.getenv("LLM_API_KEY", "") base_url: str = os.getenv("LLM_BASE_URL", "https://api.openai.com/v1") model: str = os.getenv("LLM_MODEL", "gpt-4o-mini") settings = Settings()4.2 定义请求数据结构
使用 Pydantic 定义接口的输入输出:
# app/models.py from typing import List, Optional from pydantic import BaseModel, Field class ChatMessage(BaseModel): role: str = Field(..., description="角色:system/user/assistant/tool") content: Optional[str] = Field(None, description="消息内容") name: Optional[str] = Field(None, description="工具调用时可带") class ChatRequest(BaseModel): messages: List[ChatMessage] temperature: float = 0.7 stream: bool = False class ChatResponse(BaseModel): reply: Optional[str] = None tool_calls: Optional[list] = None这里把tool_calls放在响应结构里,是为后面 Agent 化预留位置。
4.3 模型网关
模型网关是“石头汤”里最重要的一层抽象。后续接任何模型,都先把协议转换成 OpenAI 兼容格式:
# app/gateway.py from openai import OpenAI from app.config import settings class ModelGateway: def __init__(self): self.client = OpenAI( base_url=settings.base_url, api_key=settings.api_key, ) self.model = settings.model def chat(self, messages, temperature=0.7, tools=None): kwargs = {} if tools: kwargs["tools"] = tools response = self.client.chat.completions.create( model=self.model, messages=messages, temperature=temperature, **kwargs, ) return response.choices[0].messagegateway.chat()返回的是 SDK 的 message 对象,它包含role、content、tool_calls等属性。把模型调用隔离在这个类里,上层业务不关心背后是哪一个模型。
4.4 工具注册中心
接下来是 Agent 能力的雏形:工具注册中心。它的作用是把“能调用的外部能力”统一注册起来,并生成模型所需的结构化描述。
# app/tools.py import json from typing import Callable, Dict class ToolRegistry: def __init__(self): self._functions: Dict[str, Callable] = {} self._schemas: Dict[str, dict] = {} def register(self, name: str, description: str, parameters: dict): def decorator(func: Callable): self._functions[name] = func self._schemas[name] = { "type": "function", "function": { "name": name, "description": description, "parameters": parameters, }, } return func return decorator def get_schemas(self): return list(self._schemas.values()) def call(self, name: str, arguments: str): if name not in self._functions: raise ValueError(f"unknown tool {name}") args = json.loads(arguments) if isinstance(arguments, str) else arguments return self._functions[name](**args) tool_registry = ToolRegistry()这里最值得注意的是register的设计:业务函数只需要通过装饰器注册,就会自动生成工具描述和可调用映射。之后每加一个新的工具,不需要改核心逻辑,只需要新增一个函数。
4.5 注册一个示例工具
我们注册一个“获取服务器时间”的小工具,用来测试工具调用链路:
# app/tools_demo.py from app.tools import tool_registry @tool_registry.register( name="get_current_time", description="获取指定时区的当前时间,参数 timezone 为 IANA 时区名,例如 Asia/Shanghai", parameters={ "type": "object", "properties": { "timezone": { "type": "string", "description": "IANA 时区名", } }, "required": ["timezone"], }, ) def get_current_time(timezone: str = "Asia/Shanghai"): from datetime import datetime from zoneinfo import ZoneInfo dt = datetime.now(ZoneInfo(timezone)) return {"timezone": timezone, "local_time": dt.isoformat()}这个工具本身很简单,但它验证了整个工具调用流程:模型识别意图 → 模型生成工具参数 → 系统调用函数 → 结果回传给模型 → 模型组织最终回答。
4.6 FastAPI 服务入口
现在把以上模块在 FastAPI 中组合起来。先写一个不包含工具调用的基础对话接口,再把它扩展成支持工具调用的版本。
# app/main.py import json from fastapi import FastAPI from app.models import ChatRequest, ChatResponse from app.gateway import ModelGateway from app.tools import tool_registry import app.tools_demo # noqa: F401 确保工具被注册 app = FastAPI(title="Stone Soup AI", version="0.1.0") gateway = ModelGateway() @app.post("/v1/chat", response_model=ChatResponse) def chat(request: ChatRequest): messages = [m.dict() for m in request.messages] # 第一轮:带上工具定义调用模型 assistant_message = gateway.chat( messages=messages, temperature=request.temperature, tools=tool_registry.get_schemas(), ) # 如果模型没有要求调用工具,直接返回 if not getattr(assistant_message, "tool_calls", None): return ChatResponse(reply=assistant_message.content or "") # 如果模型要求调用工具,进入工具执行循环 messages.append({ "role": assistant_message.role, "content": assistant_message.content, "tool_calls": [ { "id": tc.id, "type": "function", "function": { "name": tc.function.name, "arguments": tc.function.arguments, }, } for tc in assistant_message.tool_calls ], }) for tool_call in assistant_message.tool_calls: tool_name = tool_call.function.name tool_result = tool_registry.call(tool_name, tool_call.function.arguments) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "name": tool_name, "content": json.dumps(tool_result, ensure_ascii=False), }) # 把工具结果交还给模型,让它生成最终回答 final_message = gateway.chat( messages=messages, temperature=request.temperature, tools=tool_registry.get_schemas(), ) return ChatResponse(reply=final_message.content or "")4.7 运行与验证
启动服务:
uvicorn app.main:app --reload --port 8000用 curl 测试基础对话:
curl -X POST http://localhost:8000/v1/chat \ -H "Content-Type: application/json" \ -d '{ "messages": [ {"role": "user", "content": "你好,请简单介绍下你自己"} ] }'如果密钥、网络正常,你会收到模型回复。
再测试工具调用:
curl -X POST http://localhost:8000/v1/chat \ -H "Content-Type: application/json" \ -d '{ "messages": [ {"role": "user", "content": "请问现在北京时间是什么?"} ] }'正常情况下,模型会先生成一个get_current_time工具调用,然后我们执行工具、把结果回传,最后由模型组织出自然语言回复。整个链路就是 Agent 最核心的“模型 + 工具”闭环。
4.8 验证思路
你可以在终端打印中间过程,比如:
print("tool_calls:", assistant_message.tool_calls) print("tool_result:", tool_result)这能帮你确认是“模型没识别工具”还是“工具执行报错”。后续接入更复杂的 Agent 框架时,这种分步观察的习惯会很有价值。
5. 如何“加料”:三条进阶扩展路径
骨架和工具闭环跑通后,你已经有一个可以继续生长的 AI 应用了。下面三条拓展路径是 2024 年 AI 应用开发最常遇到的场景。
5.1 加向量库:从对话升级为 RAG
如果你希望 AI 回答基于自己的文档,而不是完全依赖模型内部知识,就需要引入RAG(Retrieval-Augmented Generation,检索增强生成)。核心流程是:
- 把文档切片并向量化。
- 用户提问后,先从向量库找回相关片段。
- 把片段拼入上下文,再让模型回答。
加入方式示例(伪代码):
def rag_chat(user_question: str): docs = vector_store.search(user_question, top_k=5) context = "\n\n".join(doc.page_content for doc in docs) messages = [ { "role": "system", "content": f"请基于以下资料回答用户问题:\n\n{context}", }, {"role": "user", "content": user_question}, ] return gateway.chat(messages)这里建议把“文档切分大小”“相似度阈值”“引用格式”都做成配置项,方便反复调优。
5.2 加 Agent 编排:让模型决定调用流程
工具调用是一轮“识别意图 → 调用 → 回传”。Agent 则是在这个基础上增加多步决策能力。比如用户问“帮我调查竞品新闻并生成一份摘要”,系统可能需要:
- 调用搜索工具。
- 调用网页抓取工具。
- 调用总结工具。
Stone Soup AI 的思路是继续沿用工具注册中心,把业务函数一步步加进去,然后再用更复杂的编排层控制循环次数、停止条件、异常恢复策略。
加入循环后,你的主逻辑会变成:
for _ in range(max_steps): message = gateway.chat(messages, tools=schemas) if not message.tool_calls: break # 执行所有 tool_calls,追加结果到 messages这一步值得单独做一个小项目专门练习,因为 Agent 的难点不在代码,而在“模型会连续调用多个工具后开始犯错”的状态管理。
5.3 加多模态能力:图文输入输出
多模态模型(如 GPT-4o、Claude、Qwen-VL)已经可以实现图片理解、语音输入等能力。Stone Soup 思路在这里同样适用:接口层先支持多模态消息格式,底层模型可以在多个多模态模型之间切换。你不需要重复搭建服务,只需要在消息协议里增加 image 等内容类型。
例如,把图片转为 base64,放入 user 消息:
{ "role": "user", "content": [ {"type": "text", "text": "这张图里有什么?"}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}} ] }大多数兼容 OpenAI 协议的模型服务都支持这种格式,具体字段要以模型文档为准。
6. 常见问题与排查思路
下面的表格整理了 Stone Soup AI 骨架搭建中最高频的几个问题。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 请求报 401 / Invalid API Key | 环境变量没加载,或密钥错误 | 检查.env,确认load_dotenv()已执行 |
| 模型不调用工具 | 工具描述不清晰,或模型不支持 function calling | 简化参数描述,换支持工具调用的模型 |
| 工具执行后模型继续乱答 | tool_call 结果拼接格式错误 | 检查 tool_call_id 是否一一对应 |
| 中文乱码 | JSON 序列化时 ensure_ascii 问题 | 使用json.dumps(..., ensure_ascii=False) |
| token 消耗过大 | 上下文不断累积 | 实现窗口截断或摘要压缩 |
| 不同模型返回值结构不同 | 模型协议不完全兼容 OpenAI | 在 gateway 层做适配转换 |
| 启动时工具没被注册 | 模块没有 import | 在入口模块中显式导入工具模块 |
再补充一个排查顺序清单:
- 先确认网络连通和密钥是否有效。
- 用官方 SDK 写一个最小调用,绕过业务代码定位问题。
- 打印实际发送给模型的 messages,检查是否包含工具定义。
- 打印工具执行结果,确认是不是函数自身异常。
- 最后再检查流程编排逻辑。
这个顺序能帮你把问题从“环境问题→模型问题→代码问题”逐层隔离。
7. 工程实践与生产建议
代码跑通只是第一步。下面这些工程实践,是把 Stone Soup AI 从 Demo 变成可靠系统必须考虑的内容。
7.1 密钥与配置管理
不要在代码里硬编码任何密钥。本地使用.env,生产环境使用配置中心或云厂商密钥管理服务。日志中禁止打印完整密钥和完整请求内容,尤其注意不要误把整个 messages 打到日志里。
7.2 敏感信息与安全边界
AI 应用天然会接触用户输入,必须考虑注入风险。比如用户输入中可能包含“忽略之前的指令”。工程上可以这样做:
- 系统提示词里明确边界。
- 对模型输出做内容安全过滤。
- 工具调用需要额外鉴权,特别是涉及数据库、订单、支付等敏感操作时。
- 涉及生产环境数据变更的 Agent 动作,必须走人工确认,不能由模型直接执行。
特别是当你给模型注册了“查数据库”“发邮件”这类工具时,要默认遵循最小权限原则:模型只拥有完成任务所需的最小权限,变更类操作用审批链路兜底。
7.3 成本治理
大模型调用是按 token 计费的,Stone Soup 式 AI 系统很容易越用越贵。建议从第一天就记录:
- 每次请求消耗的输入、输出 token。
- 每次工具调用产生的额外消耗。
- 模型缓存命中情况。
- 单用户、单租户的成本分摊。
当上下文过大时,引入滑动窗口、摘要压缩、向量检索代替全文拼接,都能明显降低成本。
7.4 可观测性
在 gateway 层统一打印类似这样的结构化日志:
{ "request_id": "xxx", "model": "gpt-4o-mini", "prompt_tokens": 1234, "completion_tokens": 234, "latency_ms": 1850, "tool_calls": ["get_current_time"], "success": true }有了这些数据,你才能回答“哪个请求慢”“哪个工具经常失败”“为什么这个月成本涨了 30%”。
7.5 评估与回归
AI 应用的评估是最容易被忽视的工程环节。建议建一个评测集,包含:
- 基础问答用例。
- 边界输入用例(空字符串、超长文本、恶意指令)。
- 工具调用用例。
- RAG 知识来源正确性用例。
每次升级模型、修改提示词、增加工具,都先跑一遍评测集。没有评估,你就不知道自己往锅里加的“调料”到底有没有让汤变得更好喝。
8. 总结与下一步学习方向
Stone Soup AI(2024)不是一个具体的软件包,而是一种 AI 应用架构思想:从最小可用的对话骨架出发,通过统一接口、工具注册、渐进扩展,把大模型、检索、工具、记忆等能力逐步组合成完整的业务系统。
这篇文章里,我们完成了以下关键实践:
- 搭建了 FastAPI 服务 + 模型网关的基础骨架。
- 实现了工具注册中心和工具调用闭环。
- 梳理了 RAG、Agent、多模态三条扩展路径。
- 整理了密钥、安全、成本、可观测性四个工程重点。
如果你想把这条路线继续走下去,建议按顺序研究下面几个方向:
- LangChain / LlamaIndex 的底层实现:看它们如何做模型抽象和链式编排。
- Function Calling 协议细节:不同模型对工具描述的敏感度差异。
- RAG 调优:切分策略、embedding 模型、重排机制。
- Agent 记忆与规划:如何管理多轮工具调用的状态。
- 模型评估与评测集建设:让 AI 应用质量可度量。
最后给你一个很实在的建议:别急着把架构设计得无比复杂。先把这一节代码里的小骨架复制到本地,用你手上可用的模型 API Key,把一条最简单的对话和工具调用链路跑通。等这口锅真正沸腾起来,你自然会知道下一块该往里面放什么“食材”。
如果你在复现过程中遇到了问题,可以对照第 6 节的排查清单逐步定位,也可以在评论区把你的报错信息发出来。下一篇文章我会重点拆解工具调用循环里的状态管理,以及如何用最少代码实现一个可追踪的 Agent 运行日志。