在实际的 AI 大模型应用开发中,工程边界早就不是“调一次模型接口”这么简单。一个能进入业务使用的 AI 应用,通常要把 LLM 对话能力、RAG 私有知识检索、Agent 决策、工具调用、MCP 统一接入和稳定工作流编排组合到一起。这套组合常被开发者称为大模型应用开发全家桶。下面按一条可执行的建设路线展开:先讲清楚每个模块解决什么问题,再分别用 FastAPI 和 Spring Boot 两个后端技术栈搭建最小可运行骨架,最后给出常见问题排查路径和生产化建议。适合已经会写简单接口、能看懂 HTTP 请求和 JSON 结构,但还没有完整搭过 AI 应用的开发者阅读。按这条路线走完后,可以从单点 LLM 调用开始,逐步加入 RAG、工具调用、Agent 和工作流,并迁移到团队现有技术栈。
1. 先理解 AI 大模型应用全家桶由哪些模块组成
1.1 每个模块解决什么问题
先用一句话给每个模块定位:
LLM 是能力底座,负责自然语言的理解和生成。RAG 在生成答案前先从外部知识库中检索相关内容,再拼进上下文,解决模型不知道、记不住、知识时效差的问题。工具调用通过结构化参数让模型触发外部 API 或函数,解决模型“只能说话、不能做事”的限制。Agent 围绕目标自主规划调用哪些工具,并根据工具返回结果决定下一步动作,解决复杂任务拆解问题。MCP 把外部工具和知识源的接入方式统一成一种协议,解决多个 Agent、多套工具之间的连接标准化问题。工作流把固定的业务步骤编排成可执行的流水线,解决稳定场景下的流程控制问题。
这些概念不能只靠名词理解,需要放到最小例子里。比如 RAG 的完整过程是:加载一篇 PDF,把内容切分成段落,每个段落做向量化并存入向量库。用户提问时,先把问题也向量化,然后在库里检索最相似的三段,再让 LLM 基于这三段生成回答。没有这段检索拼装过程,模型只能凭内置知识作答,私有文档里的内容它根本看不到。
下面是容易误判的地方汇总:
| 模块 | 核心目标 | 容易误解的地方 |
|---|---|---|
| LLM | 生成和理解自然语言 | 不能保证事实准确,不能直接依赖模型记忆回答业务问题 |
| RAG | 补充外部知识 | 不只是向量化检索,切分和重排对结果影响更大 |
| 工具调用 | 模型主动触发外部能力 | 参数必须严格校验,模型返回的工具参数不能直接信任 |
| Agent | 动态规划与执行 | 不是没有边界的万能体,需要步数限制和权限控制 |
| MCP | 工具接入协议标准化 | 工具少时直接用 API 更快,MCP 是规模化后的选择 |
| 工作流 | 固定步骤编排 | 与 Agent 互补,稳定任务应优先用工作流 |
1.2 FastAPI 和 Spring Boot 两种技术栈怎么选
技术栈的选择没有绝对优劣,主要看团队已有基础、部署环境和维护周期。
FastAPI 的优势在于 Python 生态与 AI 模型生态衔接直接。算法团队和数据团队通常已经熟悉 Python,做 RAG 或 Agent 原型时,能直接复用已有代码,迭代速度快。Pydantic 提供的运行时校验,对工具调用场景里的参数校验也很有帮助。
Spring Boot 的优势在于企业级基础设施成熟。如果公司核心系统是 Java,AI 应用往往要沉淀到统一的服务框架中,复用已有的权限、监控、配置中心和发布流程。Java 的编译期类型检查在大型团队协作时更友好,Spring WebMvc 或 WebFlux 对异步和高并发也有成熟支撑。
双版本维护时,代码差异不可避免,但可以控制。建议两边保持相同的模块边界:都分成配置、模型客户端、RAG 组件、工具注册中心、智能体和工作流六块。业务逻辑一致,只是技术栈实现不同,迁移成本会小很多。
1.3 推荐的学习顺序:先单点,再串联,最后编排
比较稳妥的学习路径是:
- 第一步,用一个 HTTP 请求跑通 LLM 对话。不要先引入重框架,先理解 messages 数组、输出 choices 结构、temperature 和 max_tokens 的参数含义。
- 第二步,做最小 RAG。选一份本地文档,完成切分、向量化、检索和 Prompt 拼接,观察检索结果对回答的影响。
- 第三步,实现一个工具调用。定义工具名称、描述和参数 JSON Schema,让模型在必要时返回工具调用请求。
- 第四步,把两三个工具组合成 Agent,加入循环处理和最大步数限制。
- 第五步,把固定流程做成工作流,把多个工具和一次 LLM 调用的顺序固化下来。
- 第六步,再评估是否需要 MCP。当多个 Agent、多套工具需要统一接入时,才值得引入。
这个顺序背后的逻辑是:LLM 是基础能力,RAG 解决知识注入,工具调用解决实际行动,Agent 解决动态决策,工作流解决稳定编排,MCP 解决规模化连接。跨级学习容易同时踩多层错误,排查时很难定位问题到底在哪一层。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与双技术栈项目骨架怎么设计
2.1 环境要求和版本确认
FastAPI 一侧需要 Python 运行环境、FastAPI、Uvicorn、一个 HTTP 客户端库,以及可用的模型服务地址和 API Key。Spring Boot 一侧需要 JDK、Maven 或 Gradle。不同模型服务的接口规范存在差异,落地前要确认三件事:模型服务的 base_url、鉴权方式、以及是否兼容 OpenAI 风格接口。如果使用本地模型服务,可能不需要 API Key,鉴权头和 URL 路径前缀都会不同。建议先用 curl 直连模型服务,确认返回结构后,再开始写封装代码。
常见环境清单如下:
| 项目 | FastAPI 版 | Spring Boot 版 |
|---|---|---|
| 运行语言 | Python 3.10+ | JDK 17+ |
| Web 框架 | FastAPI | Spring Boot 3.x |
| 模型服务 | OpenAI 兼容接口或本地推理服务 | 同左 |
| 向量数据库 | 学习环境用本地模式,生产选独立服务 | 同左 |
| 配置管理 | .env 或环境变量 | application.yml 或配置中心 |
| HTTP 客户端 | httpx 异步客户端 | RestClient、WebClient 或 RestTemplate |
这里没有写死版本号,落地前要结合自己的正式依赖确认。Spring Boot 3.x 里的 RestClient 使用比较简洁,如果项目版本不支持,替换为 WebClient 或 RestTemplate 即可。
2.2 统一的项目结构
双版本保持相同的模块边界,可以让学习和迁移容易很多。FastAPI 项目示例结构:
text复制ai-app/
app/
core/
config.py
logger.py
llm/
client.py
schemas.py
rag/
splitter.py
embedder.py
vector_store.py
retriever.py
agent/
agent.py
executor.py
tools/
registry.py
weather.py
workflow/
engine.py
definitions.py
api/
chat.py
rag.py
agent.py
Spring Boot 对应包结构:
text复制com.example.ai
core
AppProperties.java
llm
LLMClient.java
ChatMessage.java
rag
DocumentSplitter.java
Embedder.java
VectorStore.java
Retriever.java
agent
Agent.java
ToolExecutor.java
tool
ToolRegistry.java
WeatherTool.java
workflow
WorkflowEngine.java
controller
ChatController.java
RagController.java
AgentController.java
这样划分的好处是:API 入口层不膨胀,模型客户端只负责与模型服务通信,工具注册中心只管理工具清单,工作流引擎按顺序调用各模块。每层改动可以独立测试,排查问题时也能缩小范围。
2.3 配置管理的两种写法
FastAPI 使用环境变量加 Pydantic 设置,通常创建一个 config.py:
python复制from pydantic_settings import BaseSettings
class Settings(BaseSettings):
model_base_url: str = "http://127.0.0.1:8001/v1"
api_key: str = ""
model_name: str = "your-model-name"
embedding_model: str = "your-embedding-model"
llm_temperature: float = 0.7
llm_max_tokens: int = 2048
vector_top_k: int = 3
agent_max_iterations: int = 5
class Config:
env_file = ".env"
Spring Boot 使用 application.yml 加 @ConfigurationProperties:
yaml复制ai:
model:
base-url: http://127.0.0.1:8001/v1
api-key: ${MODEL_API_KEY:}
model-name: your-model-name
temperature: 0.7
max-tokens: 2048
rag:
embedding-model: your-embedding-model
top-k: 3
agent:
max-iterations: 5
对应的 Java 配置类:
java复制@ConfigurationProperties(prefix = "ai")
public class AppProperties {
private ModelConfig model;
private RagConfig rag;
private AgentConfig agent;
// getters and setters
}
两种方式的共同点,是把模型地址、密钥、参数全部外置,不写死在代码里。API Key 优先从环境变量读取,避免提交到代码仓库。
注意:不要把真实 API Key 直接写在配置文件的默认值里。本地学习时可以用 .env 并加入 .gitignore,生产环境建议交给配置中心或密钥管理服务管理。
3. 最小可运行的 LLM 应用:先跑通对话能力
3.1 为什么先做统一的模型调用层
RAG、Agent、工作流最终都要调用模型。如果每个模块都直接写一次 HTTP 请求,代码会快速重复。统一的 LLMClient 把模型服务地址、鉴权、超时、重试和响应解析集中在一个位置,后续模块复用同一个入口。
下面用示意代码说明思路。实际项目要根据自己的模型服务接口规范调整端点路径和请求结构。
FastAPI 版 LLM 客户端:
python复制import httpx
from typing import List, Dict
class LLMClient:
def __init__(self, base_url: str, api_key: str, model: str):
self.base_url = base_url
self.api_key = api_key
self.model = model
self.timeout = httpx.Timeout(60.0)
async def chat(
self,
messages: List[Dict[str, str]],
temperature: float = 0.7,
max_tokens: int = 2048,
) -> str:
payload = {
"model": self.model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
}
headers = {}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
f"{self.base_url}/chat/completions",
json=payload,
headers=headers,
)
response.raise_for_status()
data = response.json()
return data["choices"][0]["message"]["content"]
Spring Boot 版可以这样写:
java复制public class LLMClient {
private final RestClient restClient;
private final String baseUrl;
private final String apiKey;
private final String model;
public LLMClient(String baseUrl, String apiKey, String model) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
this.model = model;
this.restClient = RestClient.create();
}
public String chat(
List<ChatMessage> messages,
double temperature,
int maxTokens) {
Map<String, Object> payload = new HashMap<>();
payload.put("model", model);
payload.put("messages", messages.stream().map(ChatMessage::toMap).toList());
payload.put("temperature", temperature);
payload.put("max_tokens", maxTokens);
HttpHeaders headers = new HttpHeaders();
if (apiKey != null && !apiKey.isBlank()) {
headers.setBearerAuth(apiKey);
}
ResponseEntity<JsonNode> response = restClient.post()
.uri(baseUrl + "/chat/completions")
.headers(h -> headers.forEach(h::set))
.body(payload)
.retrieve()
.toEntity(JsonNode.class);
return response.getBody().at("/choices/0/message/content").asText();
}
}
两个版本都只暴露 chat 方法,上层不感知 HTTP 细节。后续 RAG 拼接上下文、Agent 传递工具结果,只需要构造 messages 数组再调用同一个方法。
3.2 FastAPI 对话接口
最小接口只需要接收消息文本,返回模型回答。temperature 和 max_tokens 可以作为可选参数,方便调试时调整。
python复制class ChatRequest(BaseModel):
message: str
temperature: float = 0.7
max_tokens: int = 2048
@app.post("/v1/chat")
async def chat(req: ChatRequest):
messages = [{"role": "user", "content": req.message}]
reply = await llm_client.chat(
messages,
temperature=req.temperature,
max_tokens=req.max_tokens,
)
return {"reply": reply}
3.3 Spring Boot 对话接口
java复制@RestController
@RequestMapping("/v1/chat")
public class ChatController {
private final LLMClient llmClient;
public ChatController(LLMClient llmClient) {
this.llmClient = llmClient;
}
@PostMapping
public Map<String, String> chat(@RequestBody ChatRequest request) {
List<ChatMessage> messages = List.of(
new ChatMessage("user", request.message())
);
String reply = llmClient.chat(
messages,
request.temperature(),
request.maxTokens()
);
return Map.of("reply", reply);
}
}
3.4 LLM 参数说明与验证方法
LLM 调用效果和几个核心参数关系很大,以下参数含义和影响值得记下来:
| 参数 | 含义 | 调大影响 | 调小影响 | 常见初始值 |
|---|---|---|---|---|
| temperature | 控制随机性 | 创意强但容易偏 | 更稳定、更保守 | 0.7 |
| max_tokens | 输出最大 token 数 | 回答更长,时延更高 | 可能截断,finish_reason 变为 length | 2048 |
| top_p | 核采样概率累积阈值 | 候选更多,输出更多样 | 输出更集中 | 0.9 |
| presence_penalty | 鼓励讨论新话题 | 容易跑题 | 更围绕已有内容 | 0 |
| frequency_penalty | 降低重复词 | 重复减少,但语义可能断续 | 可能出现重复 | 0 |
验证时,先直接用 curl 请求模型服务,再走封装的客户端:
bash复制curl http://127.0.0.1:8001/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"your-model-name","messages":[{"role":"user","content":"你好"}],"temperature":0.7,"max_tokens":512}'
如果 curl 正确而 FastAPI 或 Spring Boot 报错,优先检查 base_url 末尾的路径前缀、鉴权头和响应 JSON 字段名。模型服务返回的 choices 可能为空数组,代码里要避免越界。
注意:不要只看程序能启动就认为接入成功。先验证模型服务本身,再验证客户端解析,最后验证接口返回,三层都通过才算真正接通。
4. RAG 检索增强:让模型接入私有知识
4.1 RAG 的完整流程和为什么有效
RAG 解决的核心问题是:模型内置知识不包含企业私有文档,或者知识更新不及时。RAG 的流程是先把文档切成块,对每块做向量化,存入向量库。用户提问时,把问题做同样的向量化,检索最相似的块,拼进 Prompt 再让 LLM 生成回答。
为什么这个流程比把整篇文档直接塞进 Prompt 好?因为模型上下文窗口有限,内容越多,无关信息干扰越强。先检索再生成,相当于给模型配了一个“先查资料再作答”的过程。
一个最小 RAG 流程需要六个组件:
- 文档加载器:读取 PDF、Markdown、TXT 等格式。
- 切分器:按标题或固定长度切分成块。
- 向量化器:把文本转成向量。
- 向量存储:保存向量和原文本。
- 检索器:根据问题向量返回最相似块。
- 生成器:把命中块拼入 Prompt,调用 LLM。
4.2 切分和检索参数直接影响回答质量
切分大小、重叠长度、top_k、相似度阈值,是 RAG 调试时最先要检查的位置。
| 参数 | 含义 | 参数偏小的影响 | 参数偏大的影响 |
|---|---|---|---|
| chunk_size | 每块文本长度 | 切得碎,检索粒度细但可能丢失上下文 | 上下文完整但可能包含无关内容,向量精度下降 |
| chunk_overlap | 相邻块重叠 | 上下文断点可能截断概念 | 增加重复内容,存储和检索耗时变高 |
| top_k | 检索返回块数 | 信息少,可能缺关键依据 | 无关内容增多,干扰模型回答 |
| similarity_threshold | 相似度下限 | 过滤掉部分有效内容 | 可能导致检索结果为空 |
切分策略要根据文档类型调整。固定长度切分最简单,但可能把一句话或一张表切开。有标题结构的文档,优先按标题切分,再控制长度,这样每个块语义更完整。
4.3 FastAPI 版 RAG 示例
以下示意代码展示了一个不依赖重框架的向量检索思路。实际项目可以使用成熟的向量库和嵌入服务,重点在于理解流程。
python复制def cosine_similarity(vec_a, vec_b):
dot = sum(x * y for x, y in zip(vec_a, vec_b))
norm_a = sum(x * x for x in vec_a) ** 0.5
norm_b = sum(x * x for x in vec_b) ** 0.5
if norm_a == 0 or norm_b == 0:
return 0.0
return dot / (norm_a * norm_b)
def search(query_vector, chunk_vectors, top_k=3):
scored = []
for idx, vec in enumerate(chunk_vectors):
score = cosine_similarity(query_vector, vec)
scored.append((score, idx))
scored.sort(key=lambda x: x[0], reverse=True)
return [idx for _, idx in scored[:top_k]]
检索后拼 Prompt:
python复制def build_rag_prompt(query, hit_chunks):
context = "\n\n".join(
f"[资料{idx+1}]\n{chunk}" for idx, chunk in enumerate(hit_chunks)
)
return [
{
"role": "system",
"content": "请基于提供的资料回答。如果资料中找不到答案,请明确说明资料不足。",
},
{
"role": "user",
"content": f"资料如下:\n{context}\n\n问题:{query}",
},
]
Prompt 里必须给模型设置约束。否则模型即使看到空资料,也可能凭内置知识自由发挥,RAG 就失去了意义。
4.4 Spring Boot 版 RAG 示例
Java 侧也用同样分层,把切分、向量化、存储和检索分别做成 Spring 组件:
java复制@Service
public class Retriever {
private final Embedder embedder;
private final VectorStore vectorStore;
public List<DocumentChunk> retrieve(String query, int topK) {
List<Float> queryVector = embedder.embed(query);
return vectorStore.similaritySearch(queryVector, topK);
}
}
Java 生态中可以用 Spring AI 或 LangChain4j 等框架简化 RAG,但理解这些抽象背后的流程仍然重要。建议先用自己实现的最小流程跑通,再替换框架,否则遇到结果错误时,很难定位是切分问题、向量化问题还是检索算法问题。
4.5 怎么评估 RAG 回答质量
RAG 回答是否准确,不能只看“看起来相关”。常见评估维度:
- 命中率:标准答案中的关键内容是否出现在检索结果中。
- 召回率:正确答案相关的文档块是否被检索出来。
- 答案相关性:生成答案与问题是否匹配。
- 引用正确性:回答内容是否真正来自检索资料,而不是模型自由发挥。
离线评估时,准备一组问题-答案对,记录每个问题检索到的文档块,再计算命中率。上线前至少抽样查看几十个问题的检索结果,确认 top_k 范围内的内容确实有效。常见误区是只优化生成 Prompt,忽略了检索结果本身为空或质量差。排查 RAG 问题时要先看检索输出,再看生成输出。
5. Agent 与工具调用:让模型真正做事
5.1 Agent 的工作原理和边界
Agent 不是输入问题就自动变聪明的黑盒。它的核心工作模式可以概括为:模型先判断现在是否需要调用工具,执行层调用对应函数并把结果返回给模型,模型根据结果继续规划或给出最终答案。这个过程会循环,直到模型认为任务完成或达到最大步数。
常见实现模式是 ReAct:每次模型输出包含思考、动作、动作输入,执行层解析并调用工具,把观察结果拼回上下文。对工程落地而言,更重要的是给 Agent 划边界:工具白名单、最大迭代次数、授权确认、超时和审计日志。
如果 Agent 能调用企业内部的查询、发消息等工具,就必须有权限校验。工具可以执行,但不代表当前用户有权限执行。这个校验要在工具调用入口做,不能依赖模型自觉。
5.2 工具注册表:统一管理工具的元数据
工具调用首先要让模型知道有哪些工具、每个工具能做什么、参数是什么。
FastAPI 版工具示例:
python复制class Tool:
def __init__(self, name, description, parameters, func):
self.name = name
self.description = description
self.parameters = parameters
self.func = func
def build_tool_schema(tool):
return {
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.parameters,
},
}
def get_weather(city: str) -> str:
return f"城市{city}天气:晴朗,25度。"
参数 JSON Schema 示例:
json复制{
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名,例如北京"}
},
"required": ["city"]
}
Spring Boot 版同样可以把工具集中在注册表里维护,区别只是注解和依赖注入方式不同。这里强调的中心思想是:工具层只做功能执行,Agent 层只做决策循环,避免两者耦合。
5.3 工具调用循环
Agent 执行循环的伪代码如下:
text复制while step < max_steps:
response = llm.chat(messages + tool_schemas)
if response contains tool_calls:
for each tool_call:
validate params
result = execute(tool_call.name, tool_call.args)
messages.append(tool_message(tool_call.id, result))
else:
return response.content
return "达到最大迭代次数,停止执行"
三个关键点必须落实:工具执行失败时,不要把异常直接抛给用户,而是把错误信息作为观察结果回传给模型,让它决定重试、换工具还是放弃;模型返回工具参数时可能不是合法 JSON,要捕获解析异常并让模型修正;最大步数限制必须存在,否则模型可能反复调用同一个工具,成本不可控。
5.4 FastAPI 和 Spring Boot 的 Agent 执行器
FastAPI 版:
python复制class AgentExecutor:
def __init__(self, llm: LLMClient, tools: List[Tool], max_steps: int = 5):
self.llm = llm
self.tools = {t.name: t for t in tools}
self.max_steps = max_steps
async def run(self, user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
tool_schemas = [build_tool_schema(t) for t in self.tools.values()]
for _ in range(self.max_steps):
reply, tool_calls = await self.llm.chat_with_tools(messages, tool_schemas)
if not tool_calls:
return reply
for call in tool_calls:
tool = self.tools.get(call["name"])
if tool is None:
result = "工具不存在"
else:
result = tool.func(**call["arguments"])
messages.append({
"role": "tool",
"tool_call_id": call["id"],
"content": result,
})
return "任务未在最大步数内完成"
Spring Boot 版逻辑相同,区别在于依赖注入和 Java 语法。建议把 ToolExecutor 放到 Agent 依赖中,单独测试工具执行逻辑,不要把工具调用代码散落在 Controller 里。
5.5 工具调用的高频坑
工具调用是 Agent 项目里报错最密集的环节,原因通常是以下四类:
- 工具描述不清晰,模型不知道什么场景该调用该工具。解决方式是给 description 加使用条件,遇到歧义时附带示例。
- 参数校验不严格,模型生成超出枚举值的参数后,工具执行到一半才报错。解决方式是在工具入口先校验参数,再执行业务逻辑。
- 失败信息没有回传,工具抛异常后 Agent 直接返回错误给用户,而不是把错误加入上下文继续修正。解决方式是把错误转换成 observation。
- 缺乏权限控制,所有工具对所有用户开放。解决方式是在执行前校验用户身份和工具权限。
6. MCP 与工作流编排:把能力组装成产品
6.1 MCP 是什么,什么时候需要它
MCP 全称 Model Context Protocol,目标是统一 AI 应用与外部工具、知识源之间的连接协议。可以把 MCP 理解成工具接入的统一接口:外部系统实现统一协议后,不同 AI 应用都能通过同一套方式连接,而不是每个工具单独写一套私有连接器。
需要明确的是,MCP 不是小项目起步的必需品。只有一个 Agent、两三个工具时,直接用 HTTP 或函数调用更快。当工具数量增多,多个 Agent 需要复用同一组工具,或者工具由不同团队维护时,MCP 的标准化价值才会体现出来。
引入 MCP 时要规划三部分:MCP 服务端负责封装工具能力,MCP 客户端负责在应用内注册和调用工具,应用层通过统一协议获取工具列表并发起调用。接入前先确认目标环境的网络策略、鉴权和超时配置,不要假设内网服务可以任意访问。
6.2 工作流和 Agent 怎么配合
工作流适合步骤明确的稳定业务,比如“先查用户信息,再查订单,最后生成客服回复”。这样的流程写成固定代码,每一步可控、可追踪、失败可重试。Agent 适合开放式任务,由模型自己决定调用哪些工具、按什么顺序执行。
两者不是替代关系。推荐做法是:稳定主流程用工作流,开放分支用 Agent。例如主流程固定为“解析意图 -> 调用查询工具 -> 生成回答”,当查询结果不明确时,可以交给 Agent 尝试其他检索策略。
| 维度 | 工作流 | Agent |
|---|---|---|
| 执行方式 | 固定步骤顺序 | 模型动态规划 |
| 可控性 | 高,每步可审计 | 中,行为由模型决策 |
| 排错难度 | 低,按步骤定位 | 高,需要日志串联工具调用 |
| 适用场景 | 客服、审批、定时任务 | 自动排障、复杂信息搜集、开放问答 |
| 落地要点 | 步骤定义清晰、异常分支完整 | 工具边界清晰、步数限制、权限校验 |
6.3 一个可扩展的全家桶服务架构
把前面几节的内容组合起来,一个通用架构可以这样分层:
请求进入后,先由 Controller 或 API 层接收并做参数校验,再进入服务层。服务层根据业务选择走工作流还是 Agent。工作流引擎按定义顺序调用 RAG 和工具,Agent 执行器根据模型输出调用工具,最终返回给上层。
整个过程中,所有模块都通过统一的 LLMClient 访问模型,通过统一的 ToolRegistry 访问工具,通过统一的配置对象读取参数。新增一个工具只需要在注册中心登记,新增一个工作流只需要定义步骤,不需要改动模型客户端。
生产环境还要再增加三件事:任务异步化、链路追踪和成本统计。耗时较长的 Agent 任务不要放在 HTTP 请求内同步等待,建议拆成任务队列。每次模型调用和工具调用都带上 trace_id,便于排查。每次调用同时累计 token 和工具调用次数,避免成本失控。
6.4 生产环境的额外保障
| 关注点 | 建议 | 原因 |
|---|---|---|
| 配置外置 | 密钥和模型地址放入配置中心 | 避免代码仓库泄露,支持多环境切换 |
| 日志 | 记录请求、模型响应、检索结果、工具调用 | 出问题时可回溯每一步 |
| 权限 | Agent 工具调用前校验用户权限 | 防止越权操作 |
| 限流 | 按用户、按接口限制调用频率 | 防止模型成本失控 |
| 超时 | 模型调用和工具调用统一设置超时 | 防止接口长时间挂起 |
| 回滚 | 工作流步骤支持按版本回退 | 业务变更时可快速恢复 |
| 数据保护 | 用户输入和检索内容不进不受控日志 | 防止隐私泄露 |
7. 高频问题排查与学习路线建议
7.1 一套通用的排查顺序
AI 应用比普通 Web 接口多了一个“模型不可预测”的变量,排查时需要按链路从底层到上层推进:
- 先确认模型服务可用。用 curl 直连模型服务,检查网络、端口、base_url 和鉴权头。
- 再确认配置是否正确加载。打印配置值,检查环境变量是否生效,避免修改了非当前环境的配置。
- 然后检查请求数据结构。messages 数组是否符合模型要求,工具 schema 是否合法。
- 检查 RAG 层的输出。把检索到的 chunk 打印出来,看内容和分数是否合理。
- 检查 Agent 循环。记录每一步模型输出、工具调用、工具结果,定位是哪一步出错。
- 最后看基础设施日志。包括网关、负载均衡和资源耗尽等底层问题。
7.2 高频问题速查表
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 调用模型返回 401 | API Key 或 base_url 错误 | curl 直连模型服务,打印实际配置 | 检查配置来源和环境变量 |
| 响应突然截断 | max_tokens 太小或模型输出到达上限 | 查看 finish_reason 是否等于 length | 调大 max_tokens,或拆分成多轮 |
| RAG 回答明显错误 | 检索结果不相关或为空 | 打印检索到的 chunk 和相似度 | 调整切分大小、top_k、相似度阈值 |
| Agent 不调用任何工具 | 工具描述不够明确或未传 tools 参数 | 查看请求中 tools 字段是否完整 | 优化工具描述,补充调用示例 |
| Agent 死循环调用工具 | 缺少步数限制或模型持续输出工具调用 | 查看 Agent 日志中的循环次数 | 设置 max_steps,对同一工具调用失败做限制 |
| 工具参数解析失败 | 模型返回了非法 JSON | 捕获解析异常并查看原始工具调用 | 把解析错误回传给模型,要求重新生成 |
| 向量维度不一致 | embedding 模型更换或索引导入方式错误 | 查询向量库一个样例,对比维度 | 统一 embedding 模型,重建索引 |
| MCP 连接不稳定 | 网络策略、服务地址或握手超时 | 查看 MCP 服务端日志 | 确认网络、鉴权和超时配置 |
7.3 从零开始做全家桶的学习路径
给没有完整做过 AI 应用开发的读者一条可执行路径:
- 用任意语言 HTTP 客户端跑通模型对话,记录不同 temperature 下的输出差异。
- 找一份本地 PDF 或 Markdown,完成手动切分、向量化、检索和基于资料的回答。
- 实现一个带参数的工具,并让模型在需要时调用它。工具可以选择纯本地函数,如汇率转换、日期计算。
- 把两个工具放入 Agent 循环,增加最大步数限制和失败回传机制。
- 把固定流程写成工作流,再对比同一个任务在工作流和 Agent 中的表现。
- 最后,如果要在团队内推广,再调研 MCP 的接入成本。
每一步都要留下一个可运行的独立项目,不要边学边堆代码。项目多了以后,把公共部分抽成共用组件,也就是前面提到的基础项目结构。
7.4 上线前检查清单
发布任意一个 AI 应用前,建议按以下清单逐项确认:
- 模型调用的超时时间是否设置,失败重试次数是否合理。
- 所有外部 API Key 是否通过环境变量或配置中心注入,不进入代码仓库。
- RAG 检索结果在离线测试集上的命中率是否符合预期。
- Agent 是否设置了最大迭代次数,工具调用是否有权限校验。
- 请求日志是否包含 trace_id,模型输入输出是否脱敏。
- 工作流每个步骤是否有异常分支,是否记录重试次数。
- 是否有限流规则,防止单个用户或单次任务耗尽资源。
- 是否准备模型变更、工具变更的回滚方案。
代码审查时,优先看这些位置:LLMClient 是否只封装模型通信,有没有重复的 HTTP 请求代码;ToolRegistry 是否集中管理工具,而不是散落各处;Agent 循环是否有步数上限;RAG 流程是否把检索结果与生成结果分开记录;配置项是否有默认值且不包含真实密钥。
从单点 LLM 调用到 RAG、工具调用、Agent、工作流和 MCP,整套体系的学习关键不是背概念,而是每个模块都跑通一个最小可用流程。FastAPI 和 Spring Boot 的选择不需要纠结,保持模块边界一致,业务逻辑可以平滑迁移。真正影响生产质量的是那些看似琐碎的工程点:超时、限流、日志、权限、最大步数和参数校验。先按顺序跑通每个模块,再逐项补生产保障,AI 大模型应用开发就会从“能调通”变成“能上线”。
