AI-01 接入模型:大模型 API 调用
从一个真实业务场景开始,完成安全、可测试、可降级的首次调用
接入成功不是终点,能够稳定地服务业务才算完成
在网页中输入一句话并得到模型回复,只能说明接口能够访问。一个真正可以放进项目的 AI 功能,还需要回答:它解决了什么业务问题?密钥是否安全?输入能否控制?超时或额度耗尽时怎么办?输出错误会不会影响核心数据?
本节以“根据用户填写的要点生成报修描述草稿”为例,完成一次从业务页面到后端、再到大模型服务的完整调用。学生也可以替换为商品描述、活动摘要、通知草稿或其他与项目有关的场景。
本节学习目标
理解大模型 API 的基本请求过程,在后端安全保存调用配置,完成一个与现有业务有关的文本生成功能,并通过输入校验、超时处理、人工确认和降级方案控制外部模型带来的风险。
返回扩展篇导读
进入下一节:实现流式问答
🎯 本节完成后,你要交付
| 成果 |
要求 |
| AI 功能任务卡 |
说明目标用户、业务问题、输入、输出、价值和明确边界 |
| 后端模型客户端 |
能通过后端调用所选模型服务,不在前端暴露 API Key |
| 可操作业务入口 |
AI 功能出现在需要它的业务页面,而不是孤立的聊天演示 |
| 异常与降级处理 |
超时、配置缺失、额度不足或服务失败时不阻断核心业务 |
| 测试记录 |
至少覆盖正常、空输入、超长输入、服务失败和人工确认 |
| 配置说明 |
提供环境变量名称和配置步骤,不包含任何真实密钥 |
扩展功能不能阻碍基础项目
只有在核心业务闭环、测试和部署已经完成后,才建议进入本节。模型服务不可用时,用户仍应能够手工填写内容并完成原有业务。
一、先判断:这个问题真的需要大模型吗
大模型擅长理解和生成自然语言,但不是所有“智能功能”都应该调用模型。
1. 适合首次接入的场景
| 场景 |
用户输入 |
模型输出 |
人工确认方式 |
| 报修描述优化 |
地点、故障现象、补充说明 |
结构清晰的报修描述草稿 |
用户编辑后提交 |
| 商品描述生成 |
商品名称、成色、特点 |
商品介绍草稿 |
卖家确认后发布 |
| 活动通知生成 |
时间、地点、对象、事项 |
活动通知草稿 |
管理员确认后发送 |
| 长文本摘要 |
已有文章或会议记录 |
简短摘要 |
用户对照原文确认 |
| 内容标签建议 |
标题和描述 |
建议标签 |
用户选择或修改 |
这些场景共同具备三个特点:
- 输入和输出都是文本,接口容易设计;
- 输出是“草稿”或“建议”,错误不会直接改变核心业务状态;
- 用户可以在保存或发布前检查和修改。
2. 不适合首次接入的场景
- 自动批准申请、退款、处罚或修改库存;
- 根据模型输出直接改变用户权限;
- 对医疗、法律、财务等高风险问题给出确定结论;
- 只用简单条件判断就能稳定完成的分类;
- 没有业务用途的通用聊天窗口;
- 必须上传大量个人隐私或敏感资料才能工作。
先比较简单方案
如果关键词、枚举值、SQL 查询或普通业务规则能够稳定解决问题,应优先使用确定性方案。大模型适合处理语言的不确定性,不适合替代所有业务规则。
二、填写 AI 功能任务卡
开始编码前,先把功能范围压缩到一个可以验收的小任务。
| 项目 |
内容 |
| 功能名称 |
【填写】 |
| 目标用户 |
【谁会使用】 |
| 当前问题 |
【用户现在遇到什么困难】 |
| 使用位置 |
【核心业务的哪一个页面或步骤】 |
| 用户输入 |
【用户主动提供什么】 |
| 系统补充 |
【系统是否补充必要的业务数据】 |
| 模型输出 |
【草稿、摘要、分类还是建议】 |
| 预期价值 |
【怎样更快、更清楚或更一致】 |
| 人工确认 |
【谁在什么时候检查结果】 |
| 明确不做 |
【本次不实现什么】 |
| 失败降级 |
【模型不可用时用户怎样继续】 |
| 验证方法 |
【使用哪些固定案例测试】 |
示例:报修描述优化
| 功能名称:报修描述草稿生成
目标用户:提交宿舍报修的学生
当前问题:用户填写过于简单,维修人员需要反复询问
使用位置:报修提交页面的“故障描述”输入框旁
用户输入:地点、设备、故障现象和已尝试的处理
系统补充:固定的输出格式要求,不补充其他用户数据
模型输出:包含地点、现象和影响的描述草稿
预期价值:帮助用户更完整地说明问题,减少补充沟通
人工确认:用户可以编辑草稿,确认后再提交报修
明确不做:不自动判断维修方案,不自动创建报修记录
失败降级:隐藏生成结果并提示用户继续手工填写
验证方法:准备正常、缺少信息、无关输入和服务超时案例
|
三、理解一次模型 API 调用
大模型 API 本质上仍然是一次 HTTP 请求。
| 业务页面
→ 项目后端接口
→ AI 功能 Service
→ 模型客户端
→ 第三方模型 API
→ 模型客户端解析结果
→ 项目后端返回统一响应
→ 页面展示草稿
→ 用户确认后再提交原业务
|
1. 一次调用通常包含什么
| 内容 |
作用 |
示例 |
| API 地址 |
确定调用哪个服务 |
由所选平台提供 |
| API Key |
证明调用者身份 |
只保存在后端环境变量 |
| 模型名称 |
选择具体模型 |
通过配置读取 |
| 指令 |
说明模型的角色、任务和边界 |
“只生成报修描述草稿” |
| 用户输入 |
本次需要处理的内容 |
地点、设备和故障现象 |
| 参数 |
控制输出长度和随机性等 |
根据平台能力设置 |
| 响应 |
模型生成的文本或结构化结果 |
报修描述草稿 |
| 用量信息 |
估算本次调用消耗 |
输入、输出 Token 等 |
不同平台的接口路径、鉴权方式、请求字段和模型名称可能不同。下面使用常见的 OpenAI 兼容对话接口说明基本过程,实际开发时必须以所选平台的最新官方文档为准。
2. 先用命令验证服务
在写业务代码前,可以先验证 API 地址、密钥和模型名称是否正确:
| curl "${AI_BASE_URL}/v1/chat/completions" \
-H "Authorization: Bearer ${AI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "'"${AI_MODEL}"'",
"messages": [
{
"role": "system",
"content": "你是报修描述助手,只生成简洁、客观的描述草稿。"
},
{
"role": "user",
"content": "地点:3号宿舍楼302;设备:空调;现象:启动后有异响但不制冷。"
}
]
}'
|
不要把真实密钥写进命令历史、截图或教材
上面的命令从环境变量读取配置。演示和截图时应隐藏终端中的敏感信息。如果平台提供临时测试工具,也不要把生成的真实密钥提交到仓库。
如果调用失败,按以下顺序排查:
- API 地址是否包含正确的协议、域名和路径;
- API Key 是否有效,是否拥有调用权限;
- 模型名称是否存在,当前账号是否能够使用;
- 请求体是否符合平台要求;
- 网络、代理、配额和速率限制是否正常;
- 服务端返回的状态码和错误编号是什么。
四、配置 API 地址、密钥和模型
1. 为什么必须配置化
开发环境、演示环境和部署环境可能使用不同的模型服务。将配置从代码中分离,可以避免泄露密钥,也便于替换模型。
建议使用以下环境变量:
| AI_BASE_URL=模型服务基础地址
AI_API_KEY=真实密钥
AI_MODEL=模型名称
AI_TIMEOUT_SECONDS=20
|
Spring Boot 项目的 application.yml 可以只保存变量映射:
| ai:
base-url: ${AI_BASE_URL:}
api-key: ${AI_API_KEY:}
model: ${AI_MODEL:}
timeout-seconds: ${AI_TIMEOUT_SECONDS:20}
|
仓库可以提供 .env.example:
| AI_BASE_URL=
AI_API_KEY=
AI_MODEL=
AI_TIMEOUT_SECONDS=20
|
.env.example 只说明变量名称,不能填写真实值。
2. 配置安全检查
可以在提交前搜索常见字段:
| git grep -n -i -E "api[_-]?key|authorization|bearer"
|
搜索结果需要人工判断:变量名和示例占位符可以保留,真实密钥必须删除并立即撤销。仅从最新文件中删除还不够,因为密钥可能仍然存在于 Git 历史中。
五、在后端集中封装模型调用
1. 不要让前端直接调用模型服务
错误结构:
这会让 API Key 出现在前端代码或浏览器请求中,任何访问页面的人都可能取得密钥。
推荐结构:
| 浏览器
→ POST /api/ai/repair-draft
→ RepairDraftService
→ LlmClient
→ 第三方模型 API
|
这种结构能够统一处理:
- 登录和角色权限;
- 输入长度和内容校验;
- API Key 与模型配置;
- 请求超时和错误转换;
- 输出长度和格式校验;
- 日志、用量和降级;
- 后续更换模型平台。
2. 定义项目自己的输入和输出
业务接口不应直接暴露第三方平台的完整请求结构。
| public record RepairDraftRequest(
String location,
String device,
String symptom,
String attemptedAction
) {
}
|
| public record RepairDraftVO(
String draft,
boolean generatedByAi
) {
}
|
这样,页面只关心“报修草稿”业务,不需要知道模型平台的 messages、Token 或其他内部字段。
3. 在 Service 中组织业务输入
| @Service
public class RepairDraftService {
private final LlmClient llmClient;
public RepairDraftService(LlmClient llmClient) {
this.llmClient = llmClient;
}
public RepairDraftVO generate(RepairDraftRequest request) {
validate(request);
String userInput = """
地点:%s
设备:%s
故障现象:%s
已尝试处理:%s
""".formatted(
request.location(),
request.device(),
request.symptom(),
emptyAsNone(request.attemptedAction())
);
String draft = llmClient.generateText(
"你是校园报修描述助手。只根据用户提供的信息生成客观、简洁的报修描述草稿;"
+ "不得虚构故障原因、维修方案、联系方式或用户未提供的信息。",
userInput
);
return new RepairDraftVO(draft, true);
}
private void validate(RepairDraftRequest request) {
if (request == null
|| isBlank(request.location())
|| isBlank(request.device())
|| isBlank(request.symptom())) {
throw new BusinessException("请填写地点、设备和故障现象");
}
int totalLength = request.location().length()
+ request.device().length()
+ request.symptom().length()
+ emptyAsNone(request.attemptedAction()).length();
if (totalLength > 1000) {
throw new BusinessException("输入内容过长,请精简后重试");
}
}
private boolean isBlank(String value) {
return value == null || value.isBlank();
}
private String emptyAsNone(String value) {
return isBlank(value) ? "无" : value.trim();
}
}
|
以上代码重点展示模块边界和处理顺序,包名、异常类和校验方式应与自己的脚手架保持一致。
4. 模型客户端只负责外部通信
LlmClient 建议集中完成:
| 读取模型配置
→ 组装第三方请求
→ 设置鉴权和超时
→ 发送 HTTP 请求
→ 检查状态码
→ 解析模型文本
→ 校验空结果和长度
→ 转换为项目异常
|
可以先定义稳定的项目接口:
| public interface LlmClient {
String generateText(String instruction, String userInput);
}
|
再根据所选平台实现 CompatibleLlmClient。如果以后更换服务,只需替换客户端实现,不需要改动报修业务。
5. CompatibleLlmClient 实现示例
下面的实现适用于课程 Spring Boot 3 脚手架,使用 Java 17 自带的 HttpClient 和 Spring Boot 已经提供的 Jackson,不需要额外引入模型 SDK。
假设所选平台提供与 /v1/chat/completions 格式兼容的接口:
| package com.course.scaffold.module.ai.client;
import com.course.scaffold.common.exception.BusinessException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.time.Duration;
import java.util.List;
@Slf4j
@Component
public class CompatibleLlmClient implements LlmClient {
private static final int MAX_OUTPUT_LENGTH = 4000;
private final ObjectMapper objectMapper;
private final HttpClient httpClient;
private final String endpoint;
private final String apiKey;
private final String model;
private final Duration requestTimeout;
public CompatibleLlmClient(
ObjectMapper objectMapper,
@Value("${ai.base-url:}") String baseUrl,
@Value("${ai.api-key:}") String apiKey,
@Value("${ai.model:}") String model,
@Value("${ai.timeout-seconds:20}") int timeoutSeconds
) {
this.objectMapper = objectMapper;
this.apiKey = apiKey == null ? "" : apiKey.trim();
this.model = model == null ? "" : model.trim();
int safeTimeoutSeconds = Math.max(1, timeoutSeconds);
this.requestTimeout = Duration.ofSeconds(safeTimeoutSeconds);
this.httpClient = HttpClient.newBuilder()
.connectTimeout(this.requestTimeout)
.build();
String normalizedBaseUrl =
baseUrl == null ? "" : baseUrl.trim().replaceAll("/+$", "");
this.endpoint = normalizedBaseUrl + "/v1/chat/completions";
}
@Override
public String generateText(String instruction, String userInput) {
validateConfiguration();
validateInput(instruction, userInput);
ChatRequest chatRequest = new ChatRequest(
model,
List.of(
new Message("system", instruction.trim()),
new Message("user", userInput.trim())
)
);
String requestBody = serialize(chatRequest);
HttpRequest request = buildRequest(requestBody);
long startTime = System.currentTimeMillis();
HttpResponse<String> response = send(request);
long elapsedMillis = System.currentTimeMillis() - startTime;
log.info("模型调用完成: status={}, elapsedMs={}",
response.statusCode(), elapsedMillis);
checkStatus(response.statusCode());
return parseText(response.body());
}
private HttpRequest buildRequest(String requestBody) {
try {
return HttpRequest.newBuilder()
.uri(URI.create(endpoint))
.timeout(requestTimeout)
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
} catch (IllegalArgumentException e) {
throw new BusinessException(503, "AI 服务地址配置错误");
}
}
private HttpResponse<String> send(HttpRequest request) {
try {
return httpClient.send(
request,
HttpResponse.BodyHandlers.ofString()
);
} catch (HttpTimeoutException e) {
throw new BusinessException(503, "AI 生成超时,请稍后重试");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException(503, "AI 生成已中断,请稍后重试");
} catch (IOException e) {
log.warn("模型服务通信失败: exception={}",
e.getClass().getSimpleName());
throw new BusinessException(503, "AI 服务暂不可用,请稍后重试");
}
}
private void checkStatus(int statusCode) {
if (statusCode >= 200 && statusCode < 300) {
return;
}
if (statusCode == 401 || statusCode == 403) {
log.error("模型服务鉴权失败: status={}", statusCode);
throw new BusinessException(503, "AI 功能配置异常");
}
if (statusCode == 429) {
throw new BusinessException(503, "AI 请求较多,请稍后重试");
}
if (statusCode >= 500) {
throw new BusinessException(503, "AI 服务暂不可用");
}
log.warn("模型服务返回非预期状态: status={}", statusCode);
throw new BusinessException(503, "AI 生成失败,请稍后重试");
}
private String serialize(ChatRequest chatRequest) {
try {
return objectMapper.writeValueAsString(chatRequest);
} catch (JsonProcessingException e) {
log.error("模型请求序列化失败", e);
throw new BusinessException(500, "AI 请求构建失败");
}
}
private String parseText(String responseBody) {
try {
ChatResponse response =
objectMapper.readValue(responseBody, ChatResponse.class);
if (response.choices() == null || response.choices().isEmpty()
|| response.choices().get(0).message() == null) {
throw new BusinessException(503, "AI 服务未返回有效内容");
}
String content = response.choices().get(0).message().content();
if (content == null || content.isBlank()) {
throw new BusinessException(503, "AI 服务未返回有效内容");
}
String result = content.trim();
if (result.length() > MAX_OUTPUT_LENGTH) {
log.warn("模型输出超过长度限制: length={}", result.length());
throw new BusinessException(503, "AI 生成内容过长,请重试");
}
return result;
} catch (JsonProcessingException e) {
log.warn("模型响应解析失败: exception={}",
e.getClass().getSimpleName());
throw new BusinessException(503, "AI 服务返回格式异常");
}
}
private void validateConfiguration() {
if (endpoint.startsWith("/v1/")
|| apiKey.isBlank()
|| model.isBlank()) {
throw new BusinessException(503, "AI 功能尚未配置");
}
}
private void validateInput(String instruction, String userInput) {
if (instruction == null || instruction.isBlank()
|| userInput == null || userInput.isBlank()) {
throw new BusinessException("AI 生成内容不能为空");
}
}
private record ChatRequest(
String model,
List<Message> messages
) {
}
private record Message(
String role,
String content
) {
}
private record ChatResponse(
List<Choice> choices
) {
}
private record Choice(
Message message
) {
}
}
|
这段实现做了以下处理:
- 在后端读取 API 地址、密钥、模型名称和超时时间;
- 使用构造方法创建并复用
HttpClient;
- 同时设置连接超时和单次请求超时;
- 不在日志中输出 API Key、用户输入和完整响应;
- 将鉴权失败、限流、超时和服务异常转换为项目业务异常;
- 检查响应中是否存在有效文本,并限制最大输出长度;
- 收到线程中断时恢复中断标记;
- 使用内部
record 隔离第三方请求和响应结构。
不要把第三方响应原文直接返回给前端
第三方错误响应可能包含内部请求信息。服务端可以记录经过筛选的状态码和请求编号,页面只显示适合用户理解的提示。
如果模型平台的基础地址已经包含 /v1,应将 endpoint 的拼接方式调整为:
| this.endpoint = normalizedBaseUrl + "/chat/completions";
|
应先查看平台文档并确定地址含义,不要通过反复尝试猜测路径。
示例代码不应机械复制
不同平台的请求和响应格式并不完全相同。应先阅读所选平台的官方文档,再让客户端适配真实字段。不要让 AI 根据猜测编造接口路径、模型名称或响应结构。
六、设计清晰、受控的提示词
提示词不是越长越好。第一次接入时,至少说明四件事:
| 角色:模型承担什么任务
输入:允许使用哪些信息
输出:需要返回什么格式
边界:禁止虚构什么、结果怎样使用
|
1. 一个可测试的提示词
| 你是校园报修描述助手。
任务:
根据用户提供的地点、设备、故障现象和已尝试处理,
生成一段 80—150 字的中文报修描述草稿。
要求:
1. 使用客观、清晰的语言;
2. 保留用户提供的关键信息;
3. 不推测故障原因;
4. 不提供维修结论;
5. 不添加联系方式或其他未提供的信息;
6. 只输出草稿正文。
|
相比“帮我优化一下”,这个提示词更容易形成稳定输出,也更容易编写测试案例。
2. 把用户输入当作数据
用户可能在输入中写入:
模型可能受到这类提示注入影响。因此:
- 不把数据库密码、密钥和内部系统指令交给模型;
- 不允许模型自行访问用户无权读取的数据;
- 明确标记用户输入的开始和结束;
- 对输入长度、字符和业务字段进行校验;
- 模型输出只能作为草稿,不直接执行高风险操作;
- 真正的权限和业务规则始终由后端代码判断。
提示词不是安全边界
“请不要泄露数据”不能代替权限控制。模型从未获得的数据,才最不可能被模型泄露。
七、在业务页面中完成闭环
报修页面可以增加“AI 生成草稿”按钮,但必须保留原来的手工输入能力。
| 用户填写地点、设备和现象
→ 点击“生成描述草稿”
→ 页面显示加载状态
→ 后端调用模型
→ 草稿写入可编辑文本框
→ 页面提示“内容由 AI 生成,请核对”
→ 用户修改或确认
→ 用户主动提交报修
|
页面交互要求
- 输入不完整时,不发起模型调用;
- 调用期间禁用重复点击;
- 明确显示加载、成功和失败状态;
- 生成内容必须可以编辑;
- AI 生成按钮与“提交报修”按钮分开;
- 用户不使用 AI 时仍能正常提交;
- 失败时保留用户已经填写的内容;
- 不把模型原始错误、密钥或内部地址显示给用户。
推荐提示文字
| AI 将根据你填写的信息生成描述草稿,可能存在不准确或遗漏。
请在提交前核对并修改。
|
八、处理超时、限流和失败
外部模型服务不属于项目自身,失败是正常情况,必须提前设计。
| 情况 |
后端处理 |
页面提示 |
用户能否继续 |
| 未配置模型服务 |
返回“AI 功能未配置” |
暂时无法生成,请手工填写 |
能 |
| 输入不完整 |
不调用模型 |
请补充必要信息 |
能 |
| 请求超时 |
主动结束等待 |
生成超时,请稍后重试 |
能 |
| 鉴权失败 |
服务端记录错误类型 |
AI 功能暂不可用 |
能 |
| 额度或限流 |
不进行无限重试 |
请求较多,请稍后重试 |
能 |
| 返回空内容 |
按失败处理 |
未生成有效内容 |
能 |
| 返回内容过长 |
截断或拒绝结果 |
请重试或手工填写 |
能 |
| 服务完全不可用 |
关闭或隐藏 AI 入口 |
保留基础业务 |
能 |
重试原则
- 输入错误、鉴权失败和额度不足通常不应自动重试;
- 网络瞬时错误可以有限重试一次;
- 重试应设置短暂间隔,不能无限循环;
- 用户重复点击也要避免产生多次并发调用;
- 每次重试都会增加等待时间,并可能增加费用。
最小降级方案
| 模型调用成功 → 显示可编辑草稿
模型调用失败 → 显示友好提示 + 保留手工输入框
|
这已经满足课程项目的基本要求。第一版不必引入复杂的消息队列、熔断平台或多模型自动切换。
九、控制隐私、日志和调用成本
1. 最少数据原则
只发送完成任务真正需要的内容。
报修描述生成通常不需要发送:
- 用户密码或登录 Token;
- 身份证号、手机号和家庭住址;
- 完整用户表或报修历史;
- 其他用户提交的内容;
- 数据库连接信息;
- 项目源代码和服务器配置。
如果业务必须处理敏感信息,应先脱敏,并确认课程、学校和所选平台是否允许上传。
2. 日志应该记录什么
建议记录:
- 请求时间和业务场景;
- 请求是否成功;
- 模型名称或配置标识;
- 响应耗时;
- 错误类别;
- 平台返回的请求编号(如有);
- 用量信息(如平台返回)。
不建议记录:
- API Key 和鉴权请求头;
- 用户完整隐私输入;
- 包含敏感信息的完整模型输出;
- 第三方服务返回的内部调试信息。
3. 调用成本控制
- 限制输入长度和输出长度;
- 选择与任务难度匹配的模型;
- 按钮防重复点击;
- 为单个用户设置合理调用频率;
- 开发测试使用固定的小样例;
- 不把同一份无变化内容反复发送;
- 记录用量并设置平台预算或额度提醒;
- 演示前确认账号仍有可用额度。
十、用固定案例完成测试
模型输出具有一定不确定性,不适合只比较整段文字是否完全相同。应检查关键约束是否满足。
1. 最低测试集合
| 编号 |
场景 |
输入 |
预期检查 |
| AI-01 |
正常生成 |
地点、设备、现象完整 |
返回非空草稿,保留关键信息 |
| AI-02 |
缺少必填项 |
缺少故障现象 |
后端拒绝,不调用模型 |
| AI-03 |
超长输入 |
超过规定长度 |
返回明确的输入错误 |
| AI-04 |
无关或恶意指令 |
要求忽略规则或输出秘密 |
不泄露数据,不执行操作 |
| AI-05 |
模型超时 |
模拟超时 |
页面提示失败,可继续手工填写 |
| AI-06 |
配置缺失 |
不设置 API Key |
核心业务正常,AI 入口正确降级 |
| AI-07 |
空或异常响应 |
模拟空结果 |
不把空结果当作成功 |
| AI-08 |
人工修改 |
生成后编辑草稿 |
保存的是用户确认后的内容 |
2. 评价生成结果
可以使用以下检查表,不必要求每次文字完全一致:
3. 不要在自动化测试中反复调用真实付费接口
单元测试和普通 CI 测试应使用模拟客户端:
| RepairDraftService
→ FakeLlmClient 返回固定草稿
|
真实平台调用可以作为少量人工集成测试,在配置了测试密钥的受控环境中执行。这样能够让测试更快、更稳定,也避免产生不必要费用。
十一、🤖 让 AI 帮你完成接入,但必须核对
可以让编码助手基于真实项目生成接入方案:
| 请先阅读当前项目的:
1. 后端技术栈和版本;
2. 统一响应与异常处理;
3. 登录和权限实现;
4. 配置文件与环境变量方式;
5. 我要接入的模型平台官方接口文档。
我要实现:【业务场景】,模型输出只作为用户可编辑的草稿。
请先给出最小改动方案,包括:
- 页面入口;
- 项目业务接口;
- Service 与模型客户端边界;
- 环境变量;
- 输入和输出 DTO;
- 超时、错误转换和降级;
- 测试案例。
要求:
1. API Key 只能保存在后端;
2. 不改变现有核心业务规则;
3. 不虚构接口路径、模型名称和响应字段;
4. 模型不可用时,用户仍能完成原业务;
5. 每一步完成后给出人工验证方法。
先列出从项目和官方文档中读取到的依据、仍缺少的信息和计划,
不要立即修改全部代码。
|
人工必须核对:
- 依赖和代码是否匹配项目的真实版本;
- 接口路径、鉴权和字段是否来自官方文档;
- API Key 是否只在后端环境变量中;
- 输入是否包含不必要的隐私数据;
- 超时和失败能否正确返回;
- 输出是否经过人工确认;
- 测试是否包含模型不可用的情况。
十二、常见问题与修正方法
| 常见问题 |
风险 |
修正方法 |
| 在前端填写 API Key |
密钥可被任何用户取得 |
所有调用经过项目后端 |
| 把真实密钥提交到 Git |
额度被盗用,数据存在风险 |
撤销密钥、清理历史、改用环境变量 |
| Controller 直接拼装第三方请求 |
业务和平台耦合,难以测试 |
使用业务 Service 和独立客户端 |
| 只写“帮我生成一下” |
输出不稳定、难以验收 |
明确任务、输入、输出和禁止事项 |
| 将模型输出直接写入数据库 |
错误内容进入正式业务 |
先展示草稿,由用户确认 |
| 没有输入长度限制 |
等待和费用不可控 |
后端限制长度和必要字段 |
| 失败后无限重试 |
请求堆积、费用增加 |
设置超时、有限重试和降级 |
| 测试每次调用真实模型 |
测试慢、不稳定且产生费用 |
使用模拟客户端测试业务逻辑 |
| 在日志中打印完整请求 |
可能泄露隐私和密钥 |
只记录必要的状态、耗时和错误类别 |
| AI 不可用时页面无法提交 |
扩展功能阻断核心业务 |
永远保留原有手工流程 |
十三、提交前自查
业务价值
实现与安全
稳定性与验证
本节小结
第一次接入大模型 API 的核心不是记住某个平台的请求格式,而是建立一条安全、清晰、可替换的调用链:
真实业务问题 → 后端业务接口 → 独立模型客户端 → 输入与输出校验 → 人工确认 → 失败降级 → 固定案例验证
完成本节后,你已经能够为项目增加一个小而完整的 AI 文本功能。下一节将在此基础上介绍流式输出,让用户在模型生成内容时能够逐步看到结果。
进入下一节:AI-02 实现流式问答
返回扩展篇导读