最近给一个团队做技术方案梳理,要搭一套面向实际业务的 AI 大模型应用底座。最开始只是做一个 Chat 接口,后面逐渐加了知识库问答、订单查询工具、自动化流程编排,发现真正落地一个 AI 应用,远不止“调一下模型接口”这么简单。网上教程大多只讲单点模块,要么只有 Python 实现,要么只有 Java 实现,缺少一条把 LLM、RAG、Agent、MCP、工作流、工具调用串成完整体系的链路。
这篇文章把这套“全家桶”的开发思路和最小可运行实现整理出来,分成 FastAPI 和 Spring Boot 两个版本,从概念、架构、核心代码到排错思路一条龙讲清楚。无论是想入门大模型应用开发,还是准备在现有 Java 或 Python 工程里集成 AI 能力,都可以直接对照着落地。
1. AI 大模型应用开发全家桶到底是什么
1.1 从一个真实的业务需求说起
假设业务方提了这样一个需求:做一个企业内部智能助手,要能回答基于内部知识库的问题,也能帮用户查订单状态,还能在某些场景下自动执行一系列操作,比如“查一下这个客户最近订单,如果超时未发货,就把处理任务分派给售后组”。
这个需求一旦拆开,会发现它不是一个纯 Chat 接口能解决的。回答内部知识库问题,需要 RAG;查订单状态,需要工具调用;根据条件决定先后处理顺序,需要 Agent 决策或者工作流编排;如果工具体系越来越大,还要用 MCP 来统一管理。这六个能力组合在一起,就是我说的“AI 大模型应用开发全家桶”。
1.2 六个核心模块定位
把这六个模块先做一个直观定位,后面的代码都是围绕它们展开的。
- LLM:大语言模型接入层。不管使用哪家大模型,都要在代码里做一层统一封装,保证上层业务不感知具体模型厂商。
- RAG:检索增强生成。把外部知识库内容切分、向量化、检索,拼进 Prompt 让模型回答,解决“模型不知道你内部资料”的问题。
- Agent:智能体。让模型不只是“回答”,还能根据目标自主决定调用哪些工具、按什么顺序执行,典型模式是 ReAct,也就是“推理 + 行动”循环。
- MCP:模型上下文协议。把工具能力封装成标准协议服务,避免每个 Agent 都定制一套工具协议,实现工具接入层面的标准化。
- 工作流:流程编排。把固定流程拆成节点,按顺序、分支、并行、循环方式执行,适合那些不需要模型太多自由发挥的场景。
- 工具调用:Function Calling。模型输出结构化工具调用指令,代码解析后执行真实函数,再把结果返回给模型继续推理。
这里要区分两个概念:Agent 和工作流。Agent 更适合目标开放、步骤不固定的场景,模型自己决定路径;工作流更适合业务流程相对固定、需要稳定可控的场景。实际项目中两者常常结合,固定部分用工作流兜底,发散部分交给 Agent。
1.3 为什么需要 FastAPI 和 Spring Boot 双版本
很多团队现状是:Python 项目里大模型实验跑得最快,但企业核心系统往往是 Java 技术栈。如果只教 Python 版本,Java 团队没法直接落地;只教 Java 版本,Python 团队又觉得绕。
FastAPI 适合 AI 原型、算法服务、数据平台,开发效率高,异步支持好,和 Python 大模型生态天然契合。Spring Boot 适合企业后端、微服务、已有业务系统集成,稳定性、治理能力、团队熟悉度有优势。双版本的思路是同一套架构设计,两种语言各自实现,方便不同背景的开发者各取所需。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体架构与双版本设计
2.1 分层架构设计
全家桶系统可以分成四层,每一层职责独立,上层依赖下层的接口而不是具体实现。
code复制接入层 FastAPI / Spring Boot 对外 API,接收请求、返回结果
编排层 Agent 决策、工作流编排、工具调度
能力层 RAG 检索、工具执行、MCP 协议转换
基础设施 大模型 API、向量数据库、业务系统、日志与监控
接入层负责 HTTP 协议处理,比如 /chat、/rag/query、/agent/run 这些接口。编排层处理业务逻辑,决定“先做什么、后做什么、什么时候调用工具”。能力层把检索、工具执行、MCP 桥接这些能力做成可以被编排层复用的服务。基础设施是外部依赖,包括模型服务、向量库、订单系统等。
这种分层带来的好处是:如果后面要换向量数据库、换模型厂商、加新工具,基本只改能力层或基础设施层,编排逻辑不用大动。
2.2 FastAPI 版本技术栈
FastAPI 版本的核心技术栈如下:
- FastAPI + Uvicorn:提供异步 Web 服务。
- httpx:异步调用大模型 HTTP 接口。
- numpy:做简单的向量余弦相似度计算,演示 RAG 检索。
- OpenAI 兼容接口:统一对接各家模型服务。
- mcp Python SDK:实现 MCP Server 工具接入。
生产环境建议把内存向量存储替换为 pgvector、Milvus、Elasticsearch 或 Qdrant。示例代码先用内存实现,重点把流程讲清楚。
2.3 Spring Boot 版本技术栈
Spring Boot 版本的核心技术栈如下:
- Spring Boot 3.x + JDK 17:企业级后端底座。
- RestTemplate 或 WebClient:调用大模型 HTTP 接口。
- Jackson:解析 JSON 响应。
- 内存向量存储:Java 实现余弦相似度,用于演示 RAG。
- Spring AI 或 LangChain4j:生产环境可选的 AI 集成框架。
Spring AI 是 Spring 官方生态中的 AI 开发框架,支持 ChatModel、EmbeddingModel 等抽象;LangChain4j 是 Java 社区的 LangChain 类似物。这两个框架都在快速演进,本文先演示不依赖它们也能实现的核心原理,方便你理解底层逻辑。
3. 环境准备与依赖说明
3.1 基础环境清单
| 环境项 | 建议版本 | 说明 |
|---|---|---|
| Python | 3.10 及以上 | FastAPI 异步特性支持更好 |
| JDK | 17 及以上 | Spring Boot 3.x 要求 |
| Docker | 可选 | 用于启动本地向量数据库 |
| 大模型 API Key | 按所选平台申请 | 使用 OpenAI 兼容接口 |
| IDE | PyCharm / IDEA 均可 | 没有强制要求 |
版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。大模型平台只要是 OpenAI 兼容接口,配置好 Base URL、API Key、模型名即可,不需要改核心代码。
3.2 Python 项目依赖
创建一个 requirements.txt,内容如下:
code复制fastapi
uvicorn
httpx
pydantic
python-dotenv
numpy
安装命令:
bash复制pip install -r requirements.txt
如果后面要跑 MCP 示例,再单独安装 mcp 官方 Python SDK。需要注意 mcp 库版本迭代较快,不同版本 API 可能存在差异,以官方仓库 README 为准。
3.3 Java 项目依赖
Maven 项目核心依赖放在 pom.xml 中:
xml复制<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
Spring Boot 版本这里以 3.2.x 为例,实际项目请根据 Spring 官方发布情况选择合适的稳定版本,不要盲目升级,也不要使用过旧版本。
4. 核心模块原理与最小实现
4.1 LLM 统一接入层:屏蔽厂商差异
大模型接口现在基本向 OpenAI 格式看齐,/chat/completions 传 model、messages、temperature,鉴权用 Authorization: Bearer xxx。所以接入层只需要封装一个 Provider,把 Base URL、API Key、模型名放到配置里。
为什么要做统一封装?因为业务代码不应该直接散落调用模型。如果哪天要换模型、加日志、做限流、做缓存,只有一个入口时改动成本最低。
Python 封装示例,文件路径 app/llm_provider.py:
python复制import os
import httpx
class LLMProvider:
def __init__(self, base_url: str = None, api_key: str = None, model: str = None):
self.base_url = (base_url or os.getenv("LLM_BASE_URL", "https://api.openai.com/v1")).rstrip("/")
self.api_key = api_key or os.getenv("LLM_API_KEY", "")
self.model = model or os.getenv("LLM_MODEL", "gpt-4o-mini")
self.headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
async def chat(self, messages: list, tools: list = None, temperature: float = 0.3) -> dict:
payload = {
"model": self.model,
"messages": messages,
"temperature": temperature,
}
if tools:
payload["tools"] = tools
async with httpx.AsyncClient(timeout=60) as client:
resp = await client.post(
f"{self.base_url}/chat/completions",
headers=self.headers,
json=payload,
)
resp.raise_for_status()
return resp.json()
这段代码里 messages 是标准对话消息列表,tools 是函数调用描述。返回的 JSON 中,choices[0].message.content 是文本回复,choices[0].message.tool_calls 是工具调用指令。
Java 版本用 RestTemplate 实现同样的能力,文件路径 src/main/java/com/example/aifamily/client/LLMClient.java:
java复制package com.example.aifamily.client;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Map;
@Service
public class LLMClient {
private final RestTemplate restTemplate = new RestTemplate();
@Value("${ai.llm.base-url}")
private String baseUrl;
@Value("${ai.llm.api-key}")
private String apiKey;
@Value("${ai.llm.model}")
private String model;
public String chat(String systemPrompt, String userContent) {
Map<String, Object> body = Map.of(
"model", model,
"messages", List.of(
Map.of("role", "system", "content", systemPrompt),
Map.of("role", "user", "content", userContent)
)
);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(apiKey);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
ResponseEntity<JsonNode> response = restTemplate.postForEntity(
baseUrl + "/chat/completions", request, JsonNode.class);
return response.getBody()
.path("choices")
.path(0)
.path("message")
.path("content")
.asText();
}
}
这里用 RestTemplate 是为了避免 starter-webflux 与 starter-web 同时引入带来的路由栈冲突。实际项目如果已经有 WebFlux 环境,换成 WebClient 写法也完全没问题。
4.2 RAG:让模型“读到”你的知识库
RAG 的核心流程是文档加载、文本切分、向量化、向量存储、相似度检索、拼接 Prompt、生成回答。
为什么要做文本切分?因为模型上下文窗口有限,而且整篇文档检索既不经济也不精准。把知识库切成几百字的小块,检索时只挑最相关的几块内容,效果和成本都更可控。
Python 示例实现一个内存向量库,文件路径 app/vector_store.py:
python复制import numpy as np
class SimpleVectorStore:
def __init__(self):
self.documents = []
self.vectors = []
def add(self, document: str, vector: list):
self.documents.append(document)
self.vectors.append(np.array(vector, dtype=np.float32))
def search(self, query_vector: list, top_k: int = 3):
query_vec = np.array(query_vector, dtype=np.float32)
scores = []
for vec in self.vectors:
dot = np.dot(query_vec, vec)
norm = np.linalg.norm(query_vec) * np.linalg.norm(vec) + 1e-9
scores.append(dot / norm)
idx = np.argsort(scores)[::-1][:top_k]
return [(self.documents[i], float(scores[i])) for i in idx]
向量化需要调用模型的 embeddings 接口。每次检索前,把用户问题也向量化,再去库里做余弦相似度排序。生产环境不会在进程内存里保存业务文档,而是用独立的向量数据库,但检索思路完全一致。
RAG 完整回答流程,文件路径 app/rag_service.py:
python复制class RagService:
def __init__(self, llm: LLMProvider, store: SimpleVectorStore, embed_model: str):
self.llm = llm
self.store = store
self.embed_model = embed_model
async def _embed(self, texts: list) -> list:
# 使用 OpenAI 兼容的 /embeddings 接口
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.post(
f"{self.llm.base_url}/embeddings",
headers=self.llm.headers,
json={"model": self.embed_model, "input": texts},
)
resp.raise_for_status()
data = resp.json()["data"]
return [item["embedding"] for item in data]
async def answer(self, question: str, top_k: int = 3) -> str:
query_vector = (await self._embed([question]))[0]
hits = self.store.search(query_vector, top_k)
context = "\n".join([doc for doc, _ in hits])
prompt = f"""请根据下面的资料回答问题。如果资料中没有相关内容,请直接说明“知识库中未找到相关信息”。
资料:
{context}
问题:{question}
"""
messages = [
{"role": "system", "content": "你是一个严谨的知识库问答助手,只能依据资料回答。"},
{"role": "user", "content": prompt},
]
result = await self.llm.chat(messages)
return result["choices"][0]["message"]["content"]
这个例子已经把 RAG 的完整链路写出来了。初次运行时你需要先往向量库里灌一批文档数据,比如产品手册、内部规范、FAQ 等。
4.3 Agent:从“回答问题”到“动手干活”
Agent 的经典模式是 ReAct。模型每走一步,先观察当前问题,决定要不要调用工具,调用完工具后把结果放回上下文,继续推理,直到能给出最终答案。
不要一开始就把 Agent 设计得很复杂。最小可用实现就是一个循环:调模型 → 判断是否有 tool_calls → 执行工具 → 回填结果 → 继续调模型。为了控制不确定性和成本,必须设置最大循环步数。
简化版 Agent 循环,文件路径 app/agent.py:
python复制import json
from app.tools import ToolRegistry
async def run_agent(question: str, llm, tools_schema: list, max_steps: int = 5):
registry = ToolRegistry()
messages = [{"role": "user", "content": question}]
for step in range(max_steps):
resp = await llm.chat(messages, tools=tools_schema)
message = resp["choices"][0]["message"]
messages.append(message)
if not message.get("tool_calls"):
return message["content"]
for tool_call in message["tool_calls"]:
fn = tool_call["function"]
tool_name = fn["name"]
args = json.loads(fn["arguments"] if fn.get("arguments") else "{}")
result = registry.execute(tool_name, args)
messages.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"content": json.dumps(result, ensure_ascii=False),
})
return "已达到最大执行步数,请简化问题后重试。"
这里把工具调用结果以 role: "tool" 的消息追加进上下文,配合 tool_call_id 与之前的工具调用指令关联。这个格式是 OpenAI 兼容接口的标准要求。
4.4 MCP:工具接入的标准化方式
MCP 全称 Model Context Protocol,模型上下文协议。它可以理解为“AI 世界的 USB 接口”。在没有 MCP 之前,每个 Agent 接入一个工具都要写一套自定义协议;有了 MCP 之后,工具方只要实现一个 MCP Server,任何支持 MCP 的客户端都可以直接调用。
MCP 有三个核心角色:
- MCP Host:宿主应用,比如 Agent、IDE、客户端。
- MCP Client:发起连接、分发工具调用请求的一端。
- MCP Server:提供服务能力的一端,暴露出可供调用的工具列表。
在全家桶里,我们可以把订单查询、库存查询、工单创建这些能力都做成 MCP Server。这样 Agent 不需要为每个工具单独写协议代码,而是通过 MCP Client 直接发现和调用工具。
Python 官方 SDK 实现 MCP Server 非常简洁,示例代码如下:
python复制from mcp.server.fastmcp import FastMCP
mcp = FastMCP("order-service")
@mcp.tool()
def query_order(order_id: str) -> str:
"""查询订单实时状态"""
return f"订单 {order_id} 当前状态:已发货"
if __name__ == "__main__":
mcp.run(transport="stdio")
这段代码的关键是 @mcp.tool() 装饰器把普通函数暴露成标准工具。transport="stdio" 表示通过标准输入输出通信,适合本地进程接入;远程场景可以使用 streamable HTTP 或 SSE 传输。
Java 生态目前社区已经有针对 MCP 的 Java SDK 封装,但由于协议和依赖版本更新频繁,工程落地时建议先去 MCP 官方文档查看最新 SDK 接入方式,不要照搬旧博客代码。
4.5 工作流:把复杂任务编排成流水线
Agent 适合发散性任务,但有些流程必须是确定性的。比如“接收工单 → 判断类型 → 执行处理 → 通知用户”,每一步都不能让模型自由发挥,否则效率和稳定性都无法保证。
写一个极简工作流引擎,文件路径 app/workflow.py:
python复制class WorkflowNode:
def __init__(self, name, func, next_nodes=None, condition=None):
self.name = name
self.func = func
self.next_nodes = next_nodes or []
self.condition = condition
def should_continue(self, result):
if self.condition is None:
return True
return self.condition(result)
class Workflow:
def __init__(self, name, nodes):
self.name = name
self.nodes = {node.name: node for node in nodes}
def run(self, start_node, context):
context.setdefault("results", {})
queue = [self.nodes[start_node]]
while queue:
node = queue.pop(0)
result = node.func(context)
context["results"][node.name] = result
if node.should_continue(result):
queue.extend(node.next_nodes)
return context
使用示例:
python复制def step_validate(context):
return {"valid": True}
def step_create_task(context):
return {"task_id": "TASK-9527"}
def step_notify(context):
return {"notified": True}
nodes = [
WorkflowNode("validate", step_validate, next_nodes=["create_task"]),
WorkflowNode("create_task", step_create_task, next_nodes=["notify"]),
WorkflowNode("notify", step_notify),
]
result = Workflow("order_process", nodes).run("validate", {})
print(result)
条件分支可以给节点传入 condition 函数,比如“如果校验失败,就不创建任务”,这样同一个流程可以支持不同业务路径。生产环境也不需要自己造轮子,Java 可以用 Flowable、Camunda,Python 可以用 Prefect、Temporal,但原理都是节点、队列、上下文三件套。
4.6 工具调用:把模型指令变成真实动作
工具调用的核心是让模型输出结构化参数,而不是让模型直接“操作”系统。这是正确且安全的设计:模型只负责理解意图和生成参数,真正执行权限在代码手里。
标准的工具描述是一个 JSON Schema,示例:
json复制[
{
"type": "function",
"function": {
"name": "get_order_status",
"description": "查询订单当前状态",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单ID"
}
},
"required": ["order_id"]
}
}
}
]
当模型发现需要查询订单时,会在响应中返回 tool_calls。代码解析出函数名和参数,执行对应的真实函数,再把结果返回给模型。工具注册表统一管理这个过程,文件路径 app/tools.py:
python复制class ToolRegistry:
def __init__(self):
self._tools = {}
def register(self, name, func):
self._tools[name] = func
def execute(self, name, args):
if name not in self._tools:
return {"error": f"工具 {name} 不存在"}
try:
return self._tools[name](**args)
except Exception as e:
return {"error": str(e)}
def list_tools(self):
return list(self._tools.keys())
def get_order_status(order_id: str) -> dict:
# 生产环境应查询真实订单系统
return {"order_id": order_id, "status": "已发货"}
def init_registry() -> ToolRegistry:
registry = ToolRegistry()
registry.register("get_order_status", get_order_status)
return registry
在实际系统中,工具函数内部需要做权限校验、参数校验、审计日志,不能直接暴露数据库或内部接口。这一点在后面的最佳实践里会展开讲。
5. FastAPI 版本实战:搭一个可运行的全家桶
5.1 项目结构
code复制ai-family-fastapi/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── config.py
│ ├── llm_provider.py
│ ├── vector_store.py
│ ├── rag_service.py
│ ├── tools.py
│ ├── agent.py
│ └── workflow.py
├── requirements.txt
└── .env
每个文件职责单一,main.py 只负责注册路由,业务逻辑都在 service 层,方便后续加单元测试。
5.2 配置文件
.env 内容:
bash复制LLM_BASE_URL=https://api.openai.com/v1
LLM_API_KEY=sk-xxxx
LLM_MODEL=gpt-4o-mini
EMBED_MODEL=text-embedding-3-small
app/config.py 负责加载配置:
python复制import os
from dotenv import load_dotenv
load_dotenv()
class Settings:
llm_base_url = os.getenv("LLM_BASE_URL", "")
llm_api_key = os.getenv("LLM_API_KEY", "")
llm_model = os.getenv("LLM_MODEL", "gpt-4o-mini")
embed_model = os.getenv("EMBED_MODEL", "text-embedding-3-small")
settings = Settings()
5.3 核心 API 代码
app/main.py:
python复制from fastapi import FastAPI
from pydantic import BaseModel
from app.config import settings
from app.llm_provider import LLMProvider
from app.vector_store import SimpleVectorStore
from app.rag_service import RagService
from app.tools import init_registry
from app.agent import run_agent
app = FastAPI(title="AI 全家桶 FastAPI 版本")
llm = LLMProvider(
base_url=settings.llm_base_url,
api_key=settings.llm_api_key,
model=settings.llm_model,
)
vector_store = SimpleVectorStore()
rag_service = RagService(llm, vector_store, settings.embed_model)
TOOLS_SCHEMA = [
{
"type": "function",
"function": {
"name": "get_order_status",
"description": "查询订单当前状态",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "订单ID"}
},
"required": ["order_id"],
},
},
}
]
class ChatRequest(BaseModel):
messages: list
class RagRequest(BaseModel):
question: str
top_k: int = 3
class AgentRequest(BaseModel):
question: str
@app.post("/chat")
async def chat(req: ChatRequest):
result = await llm.chat(req.messages)
return {"answer": result["choices"][0]["message"]["content"]}
@app.post("/rag/query")
async def rag_query(req: RagRequest):
answer = await rag_service.answer(req.question, req.top_k)
return {"answer": answer}
@app.post("/agent/run")
async def agent_run(req: AgentRequest):
registry = init_registry()
answer = await run_agent(req.question, llm, TOOLS_SCHEMA)
return {"answer": answer}
注意这里 init_registry() 实际执行工具时用的是内部注册表,但代码走的是 Agent 循环中的 ToolRegistry.execute,所以工具加载逻辑在 agent.py 里也应该初始化好注册表。在上面的示例中,run_agent 内部已经创建了 registry,同时 get_order_status 已经注册进 TOOLS_SCHEMA,两边保持一致即可。
为了启动顺序稳定,代码里 rag_service 初始化时向量库是空的,需要先调用一个灌库函数把知识文档写入。可以直接在服务启动前执行一段脚本。
5.4 运行与验证
启动命令:
bash复制uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
接口验证:
bash复制curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"messages": [{"role": "user", "content": "用一句话介绍大模型"}]}'
预期返回:
json复制{"answer": "大模型是通过海量数据训练得到的深度学习模型,具备强大的文本生成和理解能力。"}
RAG 接口在灌入知识库后调用:
bash复制curl -X POST http://localhost:8000/rag/query \
-H "Content-Type: application/json" \
-d '{"question": "我们的退款政策是什么?", "top_k": 3}'
Agent 接口调用:
bash复制curl -X POST http://localhost:8000/agent/run \
-H "Content-Type: application/json" \
-d '{"question": "帮我查询订单 A123 的状态"}'
预期返回中会包含订单状态信息。如果模型没有进入工具调用流程,检查 Tool Schema 字段名是否与代码一致,以及模型是否支持 Function Calling。
6. Spring Boot 版本实战:Java 生态对照实现
6.1 项目结构
code复制ai-family-springboot/
├── pom.xml
└── src/main/
├── java/com/example/aifamily/
│ ├── AIFamilyApplication.java
│ ├── client/LLMClient.java
│ ├── service/RagService.java
│ ├── service/AgentService.java
│ ├── service/ToolService.java
│ ├── store/VectorStore.java
│ └── controller/AIFamilyController.java
└── resources/
└── application.yml
6.2 核心配置
application.yml:
yaml复制server:
port: 8080
ai:
llm:
base-url: https://api.openai.com/v1
api-key: sk-xxxx
model: gpt-4o-mini
embed-model: text-embedding-3-small
6.3 核心代码实现
主启动类:
java复制package com.example.aifamily;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AIFamilyApplication {
public static void main(String[] args) {
SpringApplication.run(AIFamilyApplication.class, args);
}
}
向量存储,文件路径 src/main/java/com/example/aifamily/store/VectorStore.java:
java复制package com.example.aifamily.store;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class VectorStore {
public record SearchHit(String document, double score) {}
private final List<String> documents = new ArrayList<>();
private final List<double[]> vectors = new ArrayList<>();
public void add(String document, double[] vector) {
documents.add(document);
vectors.add(vector);
}
public List<SearchHit> search(double[] queryVector, int topK) {
List<SearchHit> hits = new ArrayList<>();
for (int i = 0; i < vectors.size(); i++) {
double score = cosineSimilarity(queryVector, vectors.get(i));
hits.add(new SearchHit(documents.get(i), score));
}
hits.sort((a, b) -> Double.compare(b.score(), a.score()));
return hits.subList(0, Math.min(topK, hits.size()));
}
private double cosineSimilarity(double[] a, double[] b) {
double dot = 0, na = 0, nb = 0;
for (int i = 0; i < a.length; i++) {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-9);
}
}
工具服务,文件路径 src/main/java/com/example/aifamily/service/ToolService.java:
java复制package com.example.aifamily.service;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class ToolService {
public Object execute(String toolName, Map<String, Object> args) {
switch (toolName) {
case "queryOrder":
String orderId = (String) args.get("orderId");
return Map.of("orderId", orderId, "status", "已发货");
default:
return Map.of("error", "未知工具: " + toolName);
}
}
}
Agent 服务,文件路径 src/main/java/com/example/aifamily/service/AgentService.java:
java复制package com.example.aifamily.service;
import com.example.aifamily.client.LLMClient;
import org.springframework.stereotype.Service;
@Service
public class AgentService {
private final LLMClient llmClient;
private final ToolService toolService;
public AgentService(LLMClient llmClient, ToolService toolService) {
this.llmClient = llmClient;
this.toolService = toolService;
}
public String run(String question) {
String systemPrompt = """
你是智能助手。当用户需要查询订单状态时,请输出 JSON:
{"tool": "queryOrder", "args": {"orderId": "订单号"}}
其他类型问题直接回答。
""";
String firstResponse = llmClient.chat(systemPrompt, question);
String toolCall = extractToolCall(firstResponse);
if (toolCall == null) {
return firstResponse;
}
Object result = toolService.execute("queryOrder", Map.of("orderId", "1001"));
String finalPrompt = "工具返回结果:" + result + ",请用自然语言回复用户。";
return llmClient.chat(systemPrompt, finalPrompt);
}
private String extractToolCall(String text) {
if (text != null && text.contains("\"tool\"")) {
return text;
}
return null;
}
}
这个 Agent 实现是教学用的简化版。生产环境建议直接使用支持 Function Calling 的 JSON Schema 方式,或者引入 Spring AI,否则单纯靠 Prompt 让模型输出 JSON 不稳定。但核心思路已经表达清楚:模型生成工具指令,代码解析后执行真实方法,再让模型做最终总结。
RAG 服务,文件路径 src/main/java/com/example/aifamily/service/RagService.java:
java复制package com.example.aifamily.service;
import com.example.aifamily.client.LLMClient;
import com.example.aifamily.store.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Map;
@Service
public class RagService {
private final LLMClient llmClient;
private final VectorStore vectorStore;
private final RestTemplate restTemplate = new RestTemplate();
@Value("${ai.llm.base-url}")
private String baseUrl;
@Value("${ai.llm.api-key}")
private String apiKey;
@Value("${ai.embed-model}")
private String embedModel;
public RagService(LLMClient llmClient, VectorStore vectorStore) {
this.llmClient = llmClient;
this.vectorStore = vectorStore;
}
public String answer(String question, int topK) {
double[] queryVector = embed(question);
List<VectorStore.SearchHit> hits = vectorStore.search(queryVector, topK);
StringBuilder context = new StringBuilder();
for (VectorStore.SearchHit hit : hits) {
context.append(hit.document()).append("\n");
}
String prompt = "请根据资料回答问题,资料中没有的内容不要编造:\n" + context + "\n问题:" + question;
return llmClient.chat("你是一个知识库问答助手。", prompt);
}
private double[] embed(String text) {
Map<String, Object> body = Map.of(
"model", embedModel,
"input", List.of(text)
);
org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
headers.setBearerAuth(apiKey);
org.springframework.http.HttpEntity<Map<String, Object>> request =
new org.springframework.http.HttpEntity<>(body, headers);
com.fasterxml.jackson.databind.JsonNode resp = restTemplate.postForEntity(
baseUrl + "/embeddings", request,
com.fasterxml.jackson.databind.JsonNode.class).getBody();
double[] vector = new double[0];
if (resp != null && resp.path("data").isArray() && resp.path("data").size() > 0) {
var arr = resp.path("data").get(0).path("embedding");
vector = new double[arr.size()];
for (int i = 0; i < arr.size(); i++) {
vector[i] = arr.get(i).asDouble();
}
}
return vector;
}
}
Controller,文件路径 src/main/java/com/example/aifamily/controller/AIFamilyController.java:
java复制package com.example.aifamily.controller;
import com.example.aifamily.client.LLMClient;
import com.example.aifamily.service.AgentService;
import com.example.aifamily.service.RagService;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class AIFamilyController {
private final LLMClient llmClient;
private final RagService ragService;
private final AgentService agentService;
public AIFamilyController(LLMClient llmClient, RagService ragService, AgentService agentService) {
this.llmClient = llmClient;
this.ragService = ragService;
this.agentService = agentService;
}
@PostMapping("/chat")
public Map<String, Object> chat(@RequestBody Map<String, Object> request) {
String content = (String) ((Map<?, ?>) ((java.util.List<?>) request.get("messages"))
.get(0)).get("content");
return Map.of("answer", llmClient.chat("", content));
}
@PostMapping("/rag/query")
public Map<String, Object> ragQuery(@RequestBody Map<String, Object> request) {
String question = (String) request.get("question");
int topK = request.get("top_k") == null ? 3 : (Integer) request.get("top_k");
return Map.of("answer", ragService.answer(question, topK));
}
@PostMapping("/agent/run")
public Map<String, Object> agentRun(@RequestBody Map<String, Object> request) {
String question = (String) request.get("question");
return Map.of("answer", agentService.run(question));
}
}
6.4 运行与验证
启动命令:
bash复制mvn spring-boot:run
验证请求:
bash复制curl -X POST http://localhost:8080/api/chat \
-H "Content-Type: application/json" \
-d '{"messages": [{"role": "user", "content": "你好"}]}'
注意 Controller 里的 chat 方法只取了第一段消息做演示,真实项目应该完整解析 messages 列表并支持多轮对话。
7. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 模型接口请求超时 | 网络环境不稳定或模型推理时间过长 | 增大 httpx/RestTemplate 超时时间,排查网络连通性 |
| 返回内容乱码 | 响应编码处理不一致 | 统一使用 UTF-8,检查 JSON 解析配置 |
| RAG 检索结果不相关 | 切分粒度不合理或 Embedding 模型不匹配 | 调整 chunk 大小,尝试不同 embedding 模型,做检索效果评测 |
| Agent 一直循环不结束 | 工具调用后结果没有正确回填上下文 | 检查 tool_call_id 是否对应,确认 tool 消息格式与模型要求一致 |
| 模型不输出工具调用 | 模型不支持 Function Calling 或 tools 参数格式错误 | 换支持函数调用的模型,核对 JSON Schema 字段名 |
| 工具执行报参数错误 | 模型生成的参数与定义不完全匹配 | 工具函数内做参数类型转换和默认值兜底,不要直接强制转换 |
| MCP 连接失败 | 传输方式不匹配或 SDK 版本不兼容 | 确认 host 与 server 使用相同传输方式,按官方文档核对 SDK |
| FastAPI 启动报错 | 端口被占用或依赖版本冲突 | 换端口,重装依赖,检查 Python 版本 |
| Spring Boot 启动失败 | JDK 版本不满足或依赖冲突 | 确认 JDK 17+,检查 mvn dependency:tree |
| 内存向量库占用过高 | 文档太多且都在进程内存中 | 更换为 pgvector、Milvus 等独立向量数据库 |
| 流式输出中断 | 代理层或网关超时 | 调大反向代理超时时间,配置 SS E 心跳 |
遇到问题时,第一步要做的是定位到具体模块。先单独测 LLM 接口是否正常,再测 RAG 检索是否返回正确片段,最后再去排查 Agent 编排逻辑。不要一上来就改代码,先看日志和请求响应体。
8. 最佳实践与工程建议
8.1 配置与密钥管理
API Key 绝对不能写死在代码里。本地开发用 .env 文件,测试和生成环境使用环境变量或配置中心。提交代码之前检查是否把密钥提交到了 Git 仓库。如果怀疑密钥泄漏,立即到平台后台吊销并重新生成。
日志打印时要做脱敏处理。模型请求和响应中可能包含用户敏感信息、订单信息,不要在日志里打印完整 Prompts 和工具参数。建议实现一个统一脱敏工具,只保留必要字段。
8.2 安全边界与权限控制
工具调用是全家桶里风险最高的模块。模型只负责生成参数,工具执行前必须做权限校验。比如某个工具只有管理员能用,或者某个订单只有归属人才能查询,这些校验必须在工具函数内部完成,不能依赖模型自己去判断。
对外暴露的 HTTP 接口要加认证鉴权,JWT、OAuth 或者内部网关都可以。同时做限流防刷,尤其是调用大模型接口的路径,成本直接和调用量挂钩,建议按用户、按 IP 做 Rate Limit。
8.3 RAG 生产化改造
内存向量库只适合教学。生产环境至少要满足三个要求:一是向量数据独立存储,不占用应用进程内存;二是支持增量更新,文档变更后能删除旧切片、写入新切片;三是支持打分调优,检索结果不是只看余弦相似度,要结合 BM25 混合检索或重排序模型。
知识库文档的版本管理也很重要。文档更新后,RAG 系统要能追踪“这个回答是基于哪个版本的文档生成的”,否则业务审计时会说不清楚回答来源。
8.4 日志与可观测性
每个请求建议生成一个 request_id,贯穿 HTTP 入口、模型调用、工具执行、最终响应全链路。日志中记录模型名、token 消耗、耗时、工具名和结果状态,方便复盘和调优。
大模型应用的错误模式与传统后端不太一样。除了网络错误、参数错误,还有“回答内容不符合预期”“检索召回质量差”这类质量型问题。建议离线搭建评测集,每次修改 Prompt、切分策略或模型参数后,用同一批测试问题回归,避免“修好一个功能坏掉另一个功能”。
8.5 性能与成本控制
模型调用是最大的成本项。常用手段包括:
- 缓存:相同或相似问题命中缓存后直接返回,不必重复调模型。
- 减少上下文:不必要的背景内容不要塞进 Prompt,RAG 检索只保留最相关的片段。
- 模型分级:简单任务用便宜快速的小模型,复杂任务才用大模型。
- 异步化:不要求实时返回的场景放到后台任务执行,降低峰值压力。
8.6 依赖版本演进策略
AI 生态变化非常快,MCP、Spring AI、LangChain 这类项目的 API 可能几个月就更新一次。建议在项目文档里记录当前使用的关键依赖版本,定期跟着官方升级公告做评审,不要盲目升级到最新版,也不要长期停留在有安全问题的旧版。核心业务代码尽量少依赖上层框架,把 LLM 接入、工具注册这些关键路径做成自己的抽象,这样底层框架换版本时影响面可控。
9. 总结与后续学习路线
这篇文章的核心是把 AI 大模型应用开发的全家桶拆成六个模块:LLM、RAG、Agent、MCP、工作流、工具调用,分别讲了它们是什么、解决什么问题、以及如何在一个统一架构里协同工作。FastAPI 版本适合快速验证原型,Spring Boot 版本适合 Java 工程落地,两套实现共享同一个分层架构思想,代码可以直接对照学习。
如果是从零开始学习,建议按这个顺序走:先把 LLM 统一接入层写熟,理解消息格式和工具调用格式;再实现一个最小 RAG 闭环,吃透文档切分、向量化、检索、生成四个环节;然后写一个带最大步数限制的 Agent 循环,掌握 tool_calls 的回填逻辑;最后再去研究 MCP 和工作流引擎。每一步都动手跑通,再往下走,学习效率会高很多。
接下来可以继续关注几个方向:Spring AI 和 LangChain4j 的官方文档、MCP 协议规范与不同传输方式、pgvector 或 Milvus 的生产配置,以及 Function Calling 在复杂业务场景下的稳定性设计。AI 应用开发没有太多玄学,核心就是把模型调用、检索、工具、流程这些基础能力做扎实,再逐步叠加业务复杂度。建议先照着本文的最小系统跑通一遍,再按自己的业务场景替换成生产组件。
