项目复盘:心理健康 AI 助手
项目技术栈:Spring Boot 3 + MyBatis-Plus + Spring Security + JWT + SSE + Spring AI + MySQL + Redis 核心功能:流式 AI 对话、心理测评、咨询预约、情绪日记 我的角色:独立开发(后端 + 数据库设计 + 部署) 规模:单人 3 个月,日均对话(按你的实测填),P95 首字延迟(按你的实测填)
说明:这篇是给面试用的复盘,所以除了「做了什么」,更强调「为什么这么做、踩了什么坑、怎么验证有效」。数字请替换成你自己的实测值。
一、项目概览
1.1 要解决的问题
心理咨询资源稀缺、价格高、用户有病耻感,很多人不愿意第一时间找人聊。产品想做的是一层「低门槛的陪伴入口」:先由 AI 做情绪疏解与初步筛查,风险高的用户引导到真人咨询师。
这决定了两条设计红线:
- 不能乱说:涉及自伤、自杀倾向必须走固定话术并给出求助热线,不能让模型自由发挥。
- 隐私极敏感:聊天内容是最高敏感数据,日志、缓存、第三方调用都要考虑脱敏。
1.2 功能边界
| 模块 | 做什么 | 技术要点 |
|---|---|---|
| AI 对话 | 流式聊天、多轮上下文 | SSE + Spring AI + token 预算 |
| 心理测评 | 量表答题、自动计分、结论 | 规则引擎(不做成 AI,结果要可复现) |
| 咨询预约 | 咨询师排班、下单、状态机 | 幂等 + 分布式锁防重复预约 |
| 情绪日记 | 记录 + 趋势分析 | 定时聚合 + 图表接口 |
| 风险识别 | 关键词 + 模型双通道 | 命中即转人工、留痕审计 |
关键取舍:测评不用 AI 判分。量表计分必须确定、可追溯、能出报告,用规则引擎;AI 只负责把结论「翻译成人话」。
二、整体架构
微信小程序
│ HTTPS
▼
Nginx(SSE 专用 location:proxy_buffering off)
│
▼
Spring Boot 应用
├── Security + JWT 过滤器(支持 header 与 query 双通道)
├── ChatController ──► ChatService
│ ├── 会话/消息持久化(MySQL)
│ ├── 上下文装配(最近 N 轮 + 摘要 + 用户画像)
│ ├── 风险预检(关键词 + 小模型分类)
│ ├── Spring AI ChatClient ──► 模型网关(硅基流动 / OpenAI 兼容)
│ └── SseEmitter 逐块推送
├── 预约服务(Redisson 分布式锁 + 幂等表)
└── 定时任务(情绪趋势聚合、会话摘要压缩)
│
├── MySQL(业务数据)
└── Redis(会话缓存、限流、锁)微信小程序
│ HTTPS
▼
Nginx(SSE 专用 location:proxy_buffering off)
│
▼
Spring Boot 应用
├── Security + JWT 过滤器(支持 header 与 query 双通道)
├── ChatController ──► ChatService
│ ├── 会话/消息持久化(MySQL)
│ ├── 上下文装配(最近 N 轮 + 摘要 + 用户画像)
│ ├── 风险预检(关键词 + 小模型分类)
│ ├── Spring AI ChatClient ──► 模型网关(硅基流动 / OpenAI 兼容)
│ └── SseEmitter 逐块推送
├── 预约服务(Redisson 分布式锁 + 幂等表)
└── 定时任务(情绪趋势聚合、会话摘要压缩)
│
├── MySQL(业务数据)
└── Redis(会话缓存、限流、锁)分层原则:Controller 只做协议(SSE / 鉴权),Service 编排,AI 调用收敛到单独的 gateway 层 —— 这样换模型、加熔断只改一处。
三、数据模型
3.1 会话与消息(一对多)
conversation(会话表)
├── id
├── user_id → 用户
├── title → 会话标题(首条消息截断生成)
├── summary → 早期对话的压缩摘要
├── status → 进行中 / 已结束
├── last_message_at → 列表排序用
└── created_at
message(消息表)
├── id
├── conversation_id → 所属会话
├── role → user / assistant / system
├── content → 消息内容
├── token_count → 消耗 token 数
├── risk_level → none / low / high(风险识别结果)
└── created_atconversation(会话表)
├── id
├── user_id → 用户
├── title → 会话标题(首条消息截断生成)
├── summary → 早期对话的压缩摘要
├── status → 进行中 / 已结束
├── last_message_at → 列表排序用
└── created_at
message(消息表)
├── id
├── conversation_id → 所属会话
├── role → user / assistant / system
├── content → 消息内容
├── token_count → 消耗 token 数
├── risk_level → none / low / high(风险识别结果)
└── created_atCREATE TABLE message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
conversation_id BIGINT NOT NULL,
role VARCHAR(16) NOT NULL,
content TEXT NOT NULL,
token_count INT DEFAULT 0,
risk_level VARCHAR(16) DEFAULT 'none',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
KEY idx_conv_time (conversation_id, created_at) -- 取最近 N 条全靠它
);CREATE TABLE message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
conversation_id BIGINT NOT NULL,
role VARCHAR(16) NOT NULL,
content TEXT NOT NULL,
token_count INT DEFAULT 0,
risk_level VARCHAR(16) DEFAULT 'none',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
KEY idx_conv_time (conversation_id, created_at) -- 取最近 N 条全靠它
);设计要点:
- 会话与消息一对多,消息按
created_at排序,索引必须是(conversation_id, created_at)复合,否则「取最近 10 轮」会回表扫全表。 - 每次对话前取最近 N 条作为上下文;
token_count用于预算控制与用量统计。 - 加
summary字段而不是另建表:摘要一条会话只有一份,跟随会话生命周期。 risk_level落库:既是审计要求,也能做风险事件的统计复盘。
3.2 其他业务表
| 表名 | 说明 |
|---|---|
| user | 用户(用户名、密码 BCrypt、角色、状态) |
| assessment | 心理测评记录(量表类型、原始答案 JSON、得分、结论) |
| appointment | 咨询预约(咨询师、时段、状态机:待支付/已确认/已完成/已取消) |
| mood_diary | 情绪日记(日期、情绪类型、内容、AI 分析结果) |
| idempotent_record | 幂等表(业务 key + 结果,防重复提交/重复消费) |
| ai_usage | 用量账单(用户、模型、输入/输出 token、耗时、是否降级) |
ai_usage 是容易被忽略但面试很加分的表:没有它就无法回答「这个月 AI 花了多少钱、哪个用户用得最多」。
四、核心链路 1:SSE + Spring AI 流式对话
4.1 时序
前端 EventSource
│ GET /api/chat/stream?message=xxx(token 走 query 或先换一次性 ticket)
▼
ChatController
├─ 1. 校验 JWT → userId
├─ 2. 取/建 Conversation
├─ 3. 风险预检(关键词 + 分类),高危直接走固定话术,不调模型
├─ 4. 保存用户消息(异步)
├─ 5. 装配上下文:摘要 + 最近 N 轮 + RAG 片段
├─ 6. token 预算检查,超限先砍历史
├─ 7. 调 Spring AI,拿 Flux<String>
├─ 8. 逐块写 SseEmitter(带心跳)
├─ 9. 结束后拼接全文 → 存 assistant 消息 → 扣额度 → 记 ai_usage
└─ 10. complete()前端 EventSource
│ GET /api/chat/stream?message=xxx(token 走 query 或先换一次性 ticket)
▼
ChatController
├─ 1. 校验 JWT → userId
├─ 2. 取/建 Conversation
├─ 3. 风险预检(关键词 + 分类),高危直接走固定话术,不调模型
├─ 4. 保存用户消息(异步)
├─ 5. 装配上下文:摘要 + 最近 N 轮 + RAG 片段
├─ 6. token 预算检查,超限先砍历史
├─ 7. 调 Spring AI,拿 Flux<String>
├─ 8. 逐块写 SseEmitter(带心跳)
├─ 9. 结束后拼接全文 → 存 assistant 消息 → 扣额度 → 记 ai_usage
└─ 10. complete()4.2 为什么是 SSE 而不是普通接口
| 方式 | 体验 | 复杂度 |
|---|---|---|
| 普通接口 | 白屏等 10–30 秒,用户以为卡死 | 低 |
| SSE 流式 | 逐字出现,用户能感知「正在思考」 | 中 |
大模型的首 token 延迟通常 0.5–2 秒、完整回答要十几秒。同样的总耗时,流式能显著降低用户的中途放弃率 —— 这是选它的核心理由,而不是「技术时髦」。
4.3 代码骨架
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream(@RequestParam String message, @RequestParam String token) {
Long userId = jwtService.verify(token); // EventSource 不能带 header,走 query
SseEmitter emitter = new SseEmitter(120_000L); // 默认 30s 不够
emitter.onTimeout(emitter::complete);
emitter.onError(e -> log.warn("sse error, userId={}", userId, e));
executor.execute(() -> { // 关键:不占用 Tomcat 请求线程
try {
String reply = chatService.streamChat(userId, message, chunk -> {
try { emitter.send(SseEmitter.event().data(chunk)); }
catch (IOException ignored) { /* 客户端已断开 */ }
});
chatService.saveAssistantMessage(userId, reply);
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e); // 不要抛给全局异常处理器
}
});
return emitter;
}@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream(@RequestParam String message, @RequestParam String token) {
Long userId = jwtService.verify(token); // EventSource 不能带 header,走 query
SseEmitter emitter = new SseEmitter(120_000L); // 默认 30s 不够
emitter.onTimeout(emitter::complete);
emitter.onError(e -> log.warn("sse error, userId={}", userId, e));
executor.execute(() -> { // 关键:不占用 Tomcat 请求线程
try {
String reply = chatService.streamChat(userId, message, chunk -> {
try { emitter.send(SseEmitter.event().data(chunk)); }
catch (IOException ignored) { /* 客户端已断开 */ }
});
chatService.saveAssistantMessage(userId, reply);
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e); // 不要抛给全局异常处理器
}
});
return emitter;
}4.4 为什么用 Spring AI 而不是裸调 HTTP
- 多厂商(硅基流动 / OpenAI / 通义 / 本地 Ollama)一套接口,换模型只改配置;
Advisor机制把「记忆 / RAG / 日志」做成可插拔的链,业务代码只写.prompt().user(q).stream();@Tool直接映射到 Spring Bean,工具调用不用手写 JSON Schema。
代价是版本迭代快(1.0 前后 artifact 命名都变了),要锁版本。
五、核心链路 2:上下文与记忆管理
这是最容易出问题、也最能体现工程能力的地方。
5.1 token 预算
每次请求先算账,再决定带多少历史:
预算 = 模型上下文上限(如 32k) - 预留输出(如 4k)
开销 = 系统提示词 + 摘要 + RAG 片段 + 用户本次输入 + 历史消息
超出 → 按「先砍最老的历史 → 再砍 RAG 条数 → 最后压缩摘要」的顺序降级预算 = 模型上下文上限(如 32k) - 预留输出(如 4k)
开销 = 系统提示词 + 摘要 + RAG 片段 + 用户本次输入 + 历史消息
超出 → 按「先砍最老的历史 → 再砍 RAG 条数 → 最后压缩摘要」的顺序降级5.2 三级记忆
| 层级 | 内容 | 实现 |
|---|---|---|
| 短期 | 最近 10 轮原始对话 | 按 conversation_id + created_at 倒序取 |
| 中期 | 更早对话的摘要 | 超过 10 轮触发异步摘要,写回 conversation.summary |
| 长期 | 用户画像(情绪倾向、关注话题) | 定时任务聚合,作为系统提示词的一部分 |
5.3 踩过的坑
- 早期只做滑动窗口:用户聊到第 30 轮,第 1 轮说的关键信息(比如「我在准备考研」)丢了,回答开始跑偏。→ 加摘要层解决。
- 摘要没有版本:改 Prompt 后老摘要和新格式混在一起,模型理解错乱。→ 摘要加
summary_version,版本不匹配就重建。 - 摘要失败没人知道:异步任务异常被吞,会话摘要一直为空。→ 加失败重试 + 监控指标。
六、核心链路 3:RAG 知识库(心理科普 + 咨询师 FAQ)
6.1 链路
离线:文档 → 清洗(去页眉页脚)→ 切块(512 token,overlap 64)→ 向量化 → 入 Milvus/ES
在线:问题 → 向量化 → 混合检索(BM25 + 向量,RRF 融合)→ Rerank 取 top5
→ 拼进 Prompt(带编号,要求「只用资料回答并标注来源」)→ 生成离线:文档 → 清洗(去页眉页脚)→ 切块(512 token,overlap 64)→ 向量化 → 入 Milvus/ES
在线:问题 → 向量化 → 混合检索(BM25 + 向量,RRF 融合)→ Rerank 取 top5
→ 拼进 Prompt(带编号,要求「只用资料回答并标注来源」)→ 生成6.2 为什么必须做混合检索
纯向量对专有名词很弱:「SCL-90」「认知行为疗法 CBT」这类术语,向量会把语义相近但完全不同的内容召回来。加了 BM25 关键词通道后,术语命中率明显提升。
6.3 怎么验证有效
建了一套 50 条的人工标注集(问题 + 标准答案所在的文档 id),每次改切块/检索参数都跑一遍:
| 指标 | 改动前 | 改动后 |
|---|---|---|
| Recall@5 | (填) | (填) |
| 空召回率 | (填) | (填) |
| 答案忠实度(人工抽检) | (填) | (填) |
面试要点:没有评估集的 RAG 优化都是玄学。能说出「我怎么建评估集、看哪个指标」的候选人,可信度立刻不一样。
七、安全与鉴权
7.1 JWT 与 EventSource 的冲突
EventSource 不支持自定义请求头,没法带 Authorization: Bearer xxx。三种解法,我选了第三种:
| 方案 | 问题 |
|---|---|
| 把 token 放 URL query | token 会进 Nginx access log、浏览器历史,有泄露风险 |
| 用 Cookie 承载 | 需要防 CSRF,且小程序侧跨域麻烦 |
| 先换一次性 ticket | 短有效期(30s)+ 一次性,泄露窗口最小 ✅ |
7.2 越权防护
- 每次取会话都校验
conversation.user_id == 当前 userId(不能只按 conversationId 查,这是 IDOR 经典漏洞); - 消息、测评结果、预约同理;
- 工具调用(
@Tool)内部再校验一次,不信任模型给的参数。
7.3 内容安全
- 风险预检:关键词(自杀、自伤、伤人)+ 小模型分类双通道,命中即走固定话术 + 热线 + 转人工;
- 输出过滤:敏感词、医疗诊断类越界表述;
- 留痕:
risk_level落库 + 全程审计日志,合规要求。
八、性能与容量
| 关注点 | 措施 |
|---|---|
| SSE 占线程 | 业务处理丢到自定义线程池,Tomcat 请求线程立刻释放 |
| 慢在模型 | 首 token 超时 10s、总超时 120s,超时降级到固定话术 |
| 突发流量 | 用户级限流(每分钟 N 次)+ 全局并发信号量(Semaphore) |
| 重复提交 | 预约下单用「幂等表 + Redisson 分布式锁」 |
| 读多写少 | 测评题目、咨询师排班进 Redis(过期时间见下表) |
| 长尾 | 高频 FAQ 问题做结果缓存(问题归一化后哈希) |
8.1 Redis 缓存清单
| 场景 | Key | 过期 |
|---|---|---|
| 心理测评题目 | assessment:questions:{type} | 永久(题目不变) |
| 用户会话列表 | conversation:list:{userId} | 10 分钟 |
| 咨询师排班 | schedule:{date} | 1 小时 |
| 热点 AI 回复 | ai:cache:{messageHash} | 1 小时 |
8.2 可以异步化的地方
| 场景 | 方案 | 注意 |
|---|---|---|
| 保存消息 | @Async 写库 | 用有界队列 + 自定义线程池,别用默认的 SimpleAsyncTaskExecutor |
| token 统计 | 异步累加,定时批量刷 | 丢一点可接受 |
| 情绪日记分析 | 异步调 AI 分析趋势 | 要能重试 |
@Bean("chatAsyncExecutor")
public Executor chatAsyncExecutor() {
return new ThreadPoolExecutor(4, 16, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2000),
new ThreadFactoryBuilder().setNameFormat("chat-async-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()); // 满了让调用方自己跑,天然限速
}@Bean("chatAsyncExecutor")
public Executor chatAsyncExecutor() {
return new ThreadPoolExecutor(4, 16, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2000),
new ThreadFactoryBuilder().setNameFormat("chat-async-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()); // 满了让调用方自己跑,天然限速
}为什么要写这么细:
@Async不指定线程池时用的是SimpleAsyncTaskExecutor,每次调用新建线程,高并发下直接把机器打穿。这是面试官很爱追问的点。
8.3 限流
// 每个用户每分钟最多 5 次对话请求(令牌桶,Redis 实现)
if (!rateLimiter.tryAcquire("chat:" + userId, 5, Duration.ofMinutes(1))) {
throw new BizException("请求太频繁,请稍后再试");
}// 每个用户每分钟最多 5 次对话请求(令牌桶,Redis 实现)
if (!rateLimiter.tryAcquire("chat:" + userId, 5, Duration.ofMinutes(1))) {
throw new BizException("请求太频繁,请稍后再试");
}九、可观测性
| 维度 | 做法 |
|---|---|
| 链路 | traceId 贯穿「HTTP → RAG 检索 → 模型调用 → 落库」,日志用 MDC |
| 指标 | 首 token 延迟 P95、总耗时、token 消耗、失败率、降级率、空召回率 |
| 告警 | 降级率 > 5%、P95 首字延迟 > 3s、模型错误率 > 10% |
| 审计 | ai_usage 表 + 风险事件表,可按用户/时间聚合 |
一条日志大概长这样:
traceId=ab12cd chat userId=1001 convId=88 model=qwen2.5-72b
ragHits=5 emptyRecall=false promptTokens=2380 outTokens=412
firstTokenMs=760 totalMs=8900 degrade=falsetraceId=ab12cd chat userId=1001 convId=88 model=qwen2.5-72b
ragHits=5 emptyRecall=false promptTokens=2380 outTokens=412
firstTokenMs=760 totalMs=8900 degrade=false能拿出这种日志结构,面试时说服力很强 —— 说明你真的在生产环境定位过问题。
十、踩坑清单
10.1 SSE 连接断开重连
现象:前端 EventSource 偶尔断开,对话中断。
原因:SseEmitter 默认超时 30 秒,长回答超过就断。
解决:
SseEmitter emitter = new SseEmitter(120000L);
emitter.onTimeout(() -> { log.warn("SSE 超时"); emitter.complete(); });
emitter.onError(e -> log.error("SSE 异常", e));SseEmitter emitter = new SseEmitter(120000L);
emitter.onTimeout(() -> { log.warn("SSE 超时"); emitter.complete(); });
emitter.onError(e -> log.error("SSE 异常", e));配套:服务端每 15s 发一次心跳(注释行 : ping),防止中间设备按空闲超时掐连接。
10.2 Nginx 下 SSE 变成一次性返回
现象:本地正常,服务器上打字机效果没了。
原因:Nginx 默认开 proxy_buffering,把后端数据攒起来一次发。
解决:
location /api/chat/stream {
proxy_pass http://backend;
proxy_buffering off; # 关键
proxy_cache off;
proxy_read_timeout 3600s;
proxy_http_version 1.1;
proxy_set_header Connection '';
}location /api/chat/stream {
proxy_pass http://backend;
proxy_buffering off; # 关键
proxy_cache off;
proxy_read_timeout 3600s;
proxy_http_version 1.1;
proxy_set_header Connection '';
}10.3 Security + JWT 与 EventSource
现象:EventSource 不支持自定义 header,带不了 token。
解决:改为「先换一次性 ticket,再用 ticket 建流」,避免 token 出现在 URL 里被日志记录。
10.4 全局异常吞掉 SSE 流
现象:出错时前端收到 JSON,EventSource 报「响应格式错误」。
原因:@RestControllerAdvice 把 SSE 接口的异常也接管了,返回 application/json。
解决:SSE 方法内部 try-catch,用 emitter.completeWithError(e) 或推一个 event: error 的 SSE 事件,别让全局处理器碰它。
10.5 上下文 token 超限
现象:聊到后面报 400(context length exceeded)。
解决:
- 只取最近 10 轮;
- 维护 token 计数(
message.token_count),超预算先截断最早的; - 更早的对话压成
conversation.summary; - 输入侧限制单条消息长度(比如 2000 字)。
10.6 线程池被打满
现象:接口整体变慢,jstack 里大量线程卡在模型调用。
原因:每个 SSE 连接占一个 Tomcat 线程等模型返回。
解决:请求线程立刻返回 SseEmitter,业务丢到独立线程池;线程池用有界队列 + CallerRuns,并在队列水位 > 70% 时告警。
10.7 消息顺序错乱
现象:用户连发两条,回复顺序反了。
原因:两条请求并发,谁先返回谁先落库。
解决:同一会话加锁(Redisson,key = conv:{id}),或前端串行化发送。
十一、复盘:做得好的 / 可以更好的
做得好的
- 把 AI 的不确定性收敛掉了:风险识别走固定话术、测评走规则引擎,AI 只做「表达层」。
- 成本可算:
ai_usage表让每一分钱都能归因到用户和模型。 - 降级完整:模型超时 → 固定话术;RAG 空召回 → 不硬答,直接说「资料里没有」。
- 踩过的坑都沉淀成了笔记(就是这一篇)。
可以更好的
- 缺少评估体系:RAG 的评估集是后期补的,早期优化全靠感觉。
- 没有灰度:Prompt 改动直接全量,出问题只能回滚代码。
- 监控偏后:先上线再补的 token 用量和降级率监控,中间有几天是瞎的。
ai_usage写入是同步的:高并发下它会成为新的瓶颈,应该改成异步 + 批量。
十二、面试怎么讲(STAR)
12.1 一分钟版
我独立做了一个心理健康 AI 助手(Spring Boot 3 + Spring AI + MySQL + Redis,前端微信小程序)。核心是把大模型做成可控的产品能力:流式对话用 SSE + Spring AI,首字延迟压到 1 秒内;风险话术和量表计分不交给模型,保证合规和可复现;上下文做了「滑动窗口 + 摘要 + 用户画像」三级记忆,解决了长对话跑偏;成本上建了用量表,能按用户和模型归因。踩过 Nginx 缓冲、EventSource 带不了 header、全局异常吞流这几个坑,都有排查过程和记录。
12.2 三分钟版
在 1 分钟版基础上,按「问题 → 方案 → 取舍 → 数据」讲其中一条链路,比如上下文管理:
一开始只做滑动窗口取最近 10 轮,结果用户聊到 30 轮后,前面说的关键信息(在准备考研)丢了,回答开始跑偏。我的方案是加一层异步摘要:超过 10 轮就把更早的对话压缩进
conversation.summary,并在每次请求前做 token 预算,超了先砍历史、再砍 RAG 条数。取舍是摘要要多一次模型调用、有信息损耗,所以我加了summary_version,Prompt 改版后能自动重建。上线后长会话的「答非所问」反馈基本消失。
12.3 追问准备(10 个高频)
- 为什么用 SSE 不用 WebSocket?→ 单向推送够用、基于 HTTP、自带重连。
- EventSource 怎么带 token?→ 一次性 ticket,不把 JWT 放 URL。
- Nginx 为什么要关缓冲?→ 否则攒批发送,打字机效果失效。
- 全局异常为什么会破坏流?→ 返回 JSON 而非
text/event-stream。 - 上下文超限怎么办?→ 预算 + 三级记忆 + 截断顺序。
- RAG 效果怎么评估?→ 人工标注集 + Recall@K + 忠实度。
- 模型挂了怎么办?→ 超时 → 重试 → 备用模型 → 固定话术四层降级。
- 成本怎么控?→ max_tokens + 模型路由 + 缓存 + 用户配额,
ai_usage归因。 - 怎么防 Prompt 注入?→ 权限不交给模型、工具内部二次鉴权、输入输出过滤。
- 你最大的技术挑战是什么?→ 挑一个真实的、有排查过程的(推荐 10.6 线程池打满,或 10.1 断流)。
Cao Note