我们团队最近在疯狂的研究AI,期间调研学习了大量的资料,感谢大佬们的分享。
期间不仅做了几个还不错的项目,也踩坑不少,我们也发光发热,把我们总结的经验以专栏的方式分享出来,希望对大家有帮助。
这是专栏内容的第2篇,这是专栏链接,没看之前文章的朋友,建议先看之前的内容。
本节重点
熟悉 Prompt 工程和优化技巧,并设计开发一款 AI 训练营面试助手应用,实战 Spring AI 调用大模型、对话记忆、Advisor、结构化输出、自定义对话记忆、Prompt 模板等特性。
具体内容包括:
- Prompt 工程基本概念
- Prompt 优化技巧
- AI 训练营面试助手应用需求分析
- AI 训练营面试助手应用方案设计
- Spring AI ChatClient / Advisor / ChatMemory 特性
- 多轮对话 AI 应用开发
- Spring AI 自定义 Advisor
- Spring AI 结构化输出 - 恋爱报告功能
- Spring AI 对话记忆持久化
- Spring AI Prompt 模板特性
- 多模态概念和开发
友情提示:由于 AI 的更新速度飞快,随着平台 / 工具 / 技术 / 软件的更新,教程的部分细节可能会失效,所以请大家重点学习思路和方法,不要因为实操和教程不一致就过于担心,而是要学会自己阅读官方文档并查阅资料,多锻炼自己解决问题的能力。
一、Prompt 工程
基本概念
Prompt 工程(Prompt Engineering)又叫提示词工程,简单来说,就是输入给 AI 的指令。比如下面这段内容,就是提示词:
请问什么是数据库?
那为什么要叫 “工程” 呢?
因为 AI 大模型生成的内容是不确定的,构建一个能够按照预期生成内容的提示词既是一门艺术,也是一门科学。提示词的质量直接影响到 AI 大模型输出的结果,因此这也是 AI 应用开发的关键技能,很多公司专门招聘提示词工程师。
我们学习 Prompt 工程的目标是:通过精心设计和优化输入提示来引导 AI 模型生成符合预期的高质量输出
提示词分类
核心 - 基于角色的分类
在 AI 对话中,基于角色的分类是最常见的,通常存在 3 种主要类型的 Prompt:
1)用户 Prompt (User Prompt):这是用户向 AI 提供的实际问题、指令或信息,传达了用户的直接需求。用户 Prompt 告诉 AI 模型 “做什么”,比如回答问题、编写代码、生成创意内容等。
用户:帮我生成什么是数据库这道面试题的答案
2)系统 Prompt (System Prompt):这是设置 AI 模型行为规则和角色定位的隐藏指令,用户通常不能直接看到。系统 Prompt 相当于给 AI 设定人格和能力边界,即告诉 AI “你是谁?你能做什么?”。
系统:你是一位经验丰富的后端大厂面试官,擅长分析候选人的技术问题并提供建设性指导。
请以专业客观的语气回答候选人的技术疑问,必要时主动追问细节以便深入评估技术能力。不要做出主观评判,而是尊重候选人的技术思路并引导其梳理解决方案。
回答时保持技术专业性,但避免使用过于晦涩的学术术语,确保候选人能够清晰理解问题本质与优化方向。
不同的系统 Prompt 可以让同一个 AI 模型表现出完全不同的应用特性,这是构建垂直领域 AI 应用(如财务顾问、教育辅导、医疗咨询等)的关键。
3)助手 Prompt (Assistant Prompt):这是 AI 模型的响应内容。在多轮对话中,之前的助手回复也会成为当前上下文的一部分,影响后续对话的理解和生成。某些场景下,开发者可以主动预设一些助手消息作为对话历史的一部分,引导后续互动。
面试官:我是负责后端技术的面试官,很高兴和你探讨技术问题。
目前你在分布式系统设计或高并发场景中遇到过哪些具体技术挑战?
可以结合项目经历谈谈问题背景、技术选型和解决方案吗?
在实际应用中,这些不同类型的提示词往往会组合使用。举个例子,一个完整的对话可能包含:
面试官:欢迎来到技术面试环节!作为候选人,我们希望了解你在后端开发领域的技术积累。能否先介绍下你接触过的工程项目?可以从项目规模、技术栈选型和你负责的核心模块讲起。
面试官:请谈谈你在微服务架构中的实践经验。
用户:我在 XX 项目中负责过服务拆分,将单体应用拆分为用户、订单、库存等微服务...
面试官:具体怎么界定服务边界?比如订单服务和支付服务的职责划分是否有重叠?
用户:我们按业务领域划分,订单服务负责...
面试官:跨服务调用时如何处理分布式事务?你们用了 Seata 的 AT 模式还是 TCC 模式?在订单退款场景中,TCC 的空回滚问题是如何解决的
扩展知识 - 基于功能的分类
除了基于角色的分类外,我们还可以从功能角度对提示词进行分类,仅作了解即可。
1)指令型提示词(Instructional Prompts):明确告诉 AI 模型需要执行的任务,通常以命令式语句开头。
翻译以下文本为英文:春天来了,花儿开了。
2)对话型提示词(Conversational Prompts):模拟自然对话,以问答形式与 AI 模型交互。
你认为人工智能会在未来取代人类工作吗?
3)创意型提示词(Creative Prompts):引导 AI 模型进行创意内容生成,如故事、诗歌、广告文案等。
写一个发生在未来太空殖民地的短篇科幻故事,主角是一位机器人工程师。
4)角色扮演提示词(Role-Playing Prompts):让 AI 扮演特定角色或人物进行回答。
假设你是爱因斯坦,如何用简单的语言解释相对论?
5)少样本学习提示词(Few-Shot Prompts):提供一些示例,引导 AI 理解所需的输出格式和风格。
将以下句子改写为正式商务语言:
示例1:
原句:这个想法不错。
改写:该提案展现了相当的潜力和创新性。
示例2:
原句:我们明天见。
改写:期待明日与您会面,继续我们的商务讨论。
现在请改写:这个价格太高了。
扩展知识 - 基于复杂度的分类
还可以从结构复杂度的角度对提示词进行分类,仅作了解即可。
1)简单提示词(Simple Prompts):单一指令或问题,没有复杂的背景或约束条件。
什么是人工智能?
2)复合提示词(Compound Prompts):包含多个相关指令或步骤的提示词。
分析下面这段代码,解释它的功能,找出潜在的错误,并提供改进建议。
3)链式提示词(Chain Prompts):一系列连续的、相互依赖的提示词,每个提示词基于前一个提示词的输出。
第一步:生成一个科幻故事的基本情节。
第二步:基于情节创建三个主要角色,包括他们的背景和动机。
第三步:利用这些角色和情节,撰写故事的开篇段落。
4)模板提示词(Template Prompts):包含可替换变量的标准化提示词结构,常用于大规模应用。
你是一位专业的{领域}专家。请回答以下关于{主题}的问题:{具体问题}。
回答应包含{要点数量}个关键点,并使用{风格}的语言风格。
💡 模板提示词的概念还是需要了解一下的,在应用开发过程中,我们可能会用到该特性,来保证提示词是规范统一的。
Token
Token 是大模型处理文本的基本单位,可能是单词或标点符号,模型的输入和输出都是按 Token 计算的,一般 Token 越多,成本越高,并且输出速度越慢。
因此在 AI 应用开发中,了解和控制 Token 的消耗至关重要。
如何计算 Token?
首先,不同大模型对 Token 的划分规则略有不同,比如根据 OpenAI 的文档:
- 英文文本:一个 token 大约相当于 4 个字符或约 0.75 个英文单词
- 中文文本:一个汉字通常会被编码为 1-2 个 token
- 空格和标点:也会计入 token 数量
- 特殊符号和表情符号:可能需要多个 token 来表示
简单估算一下,100 个英文单词约等于 75-150 个 Token,而 100 个中文字符约等于 100-200 个 Token。
实际应用中,更推荐使用工具来估计 Prompt 的 Token 数量,比如:
- OpenAI Tokenizer:适用于 OpenAI 模型的官方 Token 计算器
- 非官方的 Token 计算器
Token 成本计算
估算成本有个公式:总成本 = (输入token数 × 输入单价) + (输出token数 × 输出单价)
不同大模型的计费都不太一样,因此要认真阅读官方文档的计费标准,比如阿里系大模型:
建议大家估算成本时,可以多去对比不同大模型的价格
Token 成本优化技巧
注意,系统提示词、用户提示词和 AI 大模型输出的内容都是消耗成本的,因此我们成本优化主要从这些角度进行。
1)精简系统提示词:移除冗余表述,保留核心指令。比如将 “你是一个非常专业、经验丰富且非常有耐心的编程导师” 简化为 “你是编程导师”。
2)定期清理对话历史:对话上下文会随着交互不断累积 Token。在长对话中,可以定期请求 AI 总结之前的对话,然后以总结替代详细历
3)使用向量检索代替直接输入:对于需要处理大量参考文档的场景,不要直接将整个文档作为 Prompt,而是使用向量数据库和检索技术(RAG)获取相关段落。后续教程会带大家实战。
4)结构化替代自然语言:使用表格、列表等结构化格式代替长段落描述。
举个例子,优化前:
请问如何制作披萨?首先需要准备面粉、酵母、水、盐、橄榄油作为基础面团材料。然后根据口味选择酱料,可以是番茄酱或白酱。接着准备奶酪,最常用的是马苏里拉奶酪。最后准备各种配料如意大利香肠、蘑菇、青椒等。
优化后:
披萨制作材料:
- 面团:面粉、酵母、水、盐、橄榄油
- 酱料:番茄酱/白酱
- 奶酪:马苏里拉
- 配料:意大利香肠、蘑菇、青椒等
如何制作?
二、Prompt 优化技巧
前面也提到了,设计 Prompt 是一门艺术,高质量的 Prompt 可以显著提升 AI 输出的质量,因此我们需要重点掌握 Prompt 优化技巧。
利用资源
1、Prompt 学习
网上和 Prompt 优化相关的资源非常丰富,几乎各大主流 AI 大模型和 AI 开发框架官方文档都有相关的介绍,推荐先阅读至少 2 篇,比如:
- Prompt Engineering Guide 提示工程指南
- OpenAI 提示词工程指南
- Spring AI 提示工程指南
- Authropic 提示词工程指南
- Authropic 提示词工程指南(开源仓库)
- 智谱 AI Prompt 设计指南
2、Prompt 提示词库
网上也有很多现成的提示词库,在自主优化提示词前,可以先尝试搜索有没有现成的提示词参考:
- 文本对话:Authropic 提示词库
- AI 绘画:Midjourney 提示词库
汇总了来自 OpenAI、Anthropic、智谱 AI 等主流 AI 服务商的提示工程最佳实践,给大家整理了一份完备的 Prompt 优化技巧。
基础提示技巧
1、明确指定任务和角色
为 AI 提供清晰的任务描述和角色定位,帮助模型理解背景和期望。
系统:你是一位经验丰富的Python教师,擅长向初学者解释编程概念。
用户:请解释 Python 中的列表推导式,包括基本语法和 2-3 个实用示例。
2、提供详细说明和具体示例
提供足够的上下文信息和期望的输出格式示例,减少模型的不确定性。
请提供一个社交媒体营销计划,针对一款新上市的智能手表。计划应包含:
1. 目标受众描述
2. 三个内容主题
3. 每个平台的内容类型建议
4. 发布频率建议
示例格式:
目标受众: [描述]
内容主题: [主题1], [主题2], [主题3]
平台策略: [平台] - [内容类型] - [频率]
3、使用结构化格式引导思维
通过列表、表格等结构化格式,使指令更易理解,输出更有条理。
分析以下公司的优势和劣势:
公司: Tesla
请使用表格格式回答,包含以下列:
- 优势(最少3项)
- 每项优势的简要分析
- 劣势(最少3项)
- 每项劣势的简要分析
- 应对建议
4、明确输出格式要求
指定输出的格式、长度、风格等要求,获得更符合预期的结果。
撰写一篇关于气候变化的科普文章,要求:
- 使用通俗易懂的语言,适合高中生阅读
- 包含5个小标题,每个标题下2-3段文字
- 总字数控制在800字左右
- 结尾提供3个可行的个人行动建议
进阶提示技巧
1、思维链提示法(Chain-of-Thought)
引导模型展示推理过程,逐步思考问题,提高复杂问题的准确性。
问题:一个商店售卖T恤,每件15元。如果购买5件以上可以享受8折优惠。小明买了7件T恤,他需要支付多少钱?
请一步步思考解决这个问题:
1. 首先计算7件T恤的原价
2. 确定是否符合折扣条件
3. 如果符合,计算折扣后的价格
4. 得出最终支付金额
2、少样本学习(Few-Shot Learning)
通过提供几个输入-输出对的示例,帮助模型理解任务模式和期望输出。
我将给你一些情感分析的例子,然后请你按照同样的方式分析新句子的情感倾向。
输入: "这家餐厅的服务太差了,等了一个小时才上菜"
输出: 负面,因为描述了长时间等待和差评服务
输入: "新买的手机屏幕清晰,电池也很耐用"
输出: 正面,因为赞扬了产品的多个方面
现在分析这个句子:
"这本书内容还行,但是价格有点贵"
3、分步骤指导(Step-by-Step)
将复杂任务分解为可管理的步骤,确保模型完成每个关键环节。
请帮我创建一个简单的网站落地页设计方案,按照以下步骤:
步骤1: 分析目标受众(考虑年龄、职业、需求等因素)
步骤2: 确定页面核心信息(主标题、副标题、价值主张)
步骤3: 设计页面结构(至少包含哪些区块)
步骤4: 制定视觉引导策略(颜色、图像建议)
步骤5: 设计行动召唤(CTA)按钮和文案
4、自我评估和修正
让模型评估自己的输出并进行改进,提高准确性和质量。
解决以下概率问题:
从一副标准扑克牌中随机抽取两张牌,求抽到至少一张红桃的概率。
首先给出你的解答,然后:
1. 检查你的推理过程是否存在逻辑错误
2. 验证你使用的概率公式是否正确
3. 检查计算步骤是否有误
4. 如果发现任何问题,提供修正后的解答
5、知识检索和引用
引导模型检索相关信息并明确引用信息来源,提高可靠性。
请解释光合作用的过程及其在植物生长中的作用。在回答中:
1. 提供光合作用的科学定义
2. 解释主要的化学反应
3. 描述影响光合作用效率的关键因素
4. 说明其对生态系统的重要性
对于任何可能需要具体数据或研究支持的陈述,请明确指出这些信息的来源,并说明这些信息的可靠性。
6、多视角分析
引导模型从不同角度、立场或专业视角分析问题,提供全面见解。
分析"城市应该禁止私家车进入市中心"这一提议:
请从以下4个不同角度分析:
1. 环保专家视角
2. 经济学家视角
3. 市中心商户视角
4. 通勤居民视角
对每个视角:
- 提供支持该提议的2个论点
- 提供反对该提议的2个论点
- 分析可能的折中方案
7、多模态思维
结合不同表达形式进行思考,如文字描述、图表结构、代码逻辑等。
设计一个智能家居系统的基础架构:
1. 首先用文字描述系统的主要功能和组件
2. 然后创建一个系统架构图(用ASCII或文本形式表示)
3. 接着提供用户交互流程
4. 最后简述实现这个系统可能面临的技术挑战
尝试从不同角度思考:功能性、用户体验、技术实现、安全性等。
提示词调试与优化
好的提示词可能很难一步到位,因此我们要学会如何持续调试和优化 Prompt。
1、迭代式提示优化
通过逐步修改和完善提示词,提高输出质量。
初始提示: 谈谈人工智能的影响。
[收到笼统回答后]
改进提示: 分析人工智能对医疗行业的三大积极影响和两大潜在风险,提供具体应用案例。
[如果回答仍然不够具体]
进一步改进: 详细分析AI在医学影像诊断领域的具体应用,包括:
1. 现有的2-3个成功商业化AI诊断系统及其准确率
2. 这些系统如何辅助放射科医生工作
3. 实施过程中遇到的主要挑战
4. 未来3-5年可能的技术发展方向
2、边界测试
通过极限情况测试模型的能力边界,找出优化空间。
尝试解决以下具有挑战性的数学问题:
证明在三角形中,三条高的交点、三条中线的交点和三条角平分线的交点在同一条直线上。
如果你发现难以直接证明:
1. 说明你遇到的具体困难
2. 考虑是否有更简单的方法或特例可以探讨
3. 提供一个思路框架,即使无法给出完整证明
3、提示词模板化
创建结构化模板,便于针对类似任务进行一致性提示,否则每次输出的内容可能会有比较大的区别,不利于调试。
【专家角色】: {领域}专家
【任务描述】: {任务详细说明}
【所需内容】:
- {要点1}
- {要点2}
- {要点3}
【输出格式】: {格式要求}
【语言风格】: {风格要求}
【限制条件】: {字数、时间或其他限制}
例如:
【专家角色】: 营养学专家
【任务描述】: 为一位想减重的上班族设计一周健康饮食计划
【所需内容】:
- 七天的三餐安排
- 每餐的大致卡路里
- 准备建议和购物清单
【输出格式】: 按日分段,每餐列出具体食物
【语言风格】: 专业但友好
【限制条件】: 考虑准备时间短,预算有限
4、错误分析与修正
系统性分析模型回答中的错误,并针对性优化提示词,这一点在我们使用 Cursor 等 AI 开发工具生成代码时非常有用。
我发现之前请你生成的Python代码存在以下问题:
1. 没有正确处理文件不存在的情况
2. 数据处理逻辑中存在边界条件错误
3. 代码注释不够详细
请重新生成代码,特别注意:
1. 添加完整的异常处理
2. 测试并确保所有边界条件
3. 为每个主要函数和复杂逻辑添加详细注释
4. 遵循PEP 8编码规范
虽然前面提到了这么多提示词优化技巧,但总结出来就一句话:任务越复杂,就越要给 Prompt 补充更多细节。
我们可以把 AI 当成人类,如果你的问题模糊不清,那么得到的答案可能就并不理想。
三、AI应用需求分析
我们知道,AI 时代下,开发应用的门槛变得越来越低了,导致市面上出现了各种具有创意的小产品。也让越来越多同学意识到,技术并不是产品成功的决定性因素,而是在于你有没有把握住用户的需求、解决用户的痛点。
因此,程序员需要重点培养需求分析能力。
怎么进行需求分析呢?这里分享一个 “三步走” 方法:获取需求 => 细化需求 => 确认需求
1、需求从哪儿来?
但是我大脑空空,没什么想法,从哪里挖掘需求呢?
其实很简单!现在有很多 AI 应用平台,比如豆包、文心一言、ChatGPT 等,这些平台上已经有了大量 AI 应用,是很好的参考源,随便挑一个都可以变成完整的项目。
比如我们来开发一个 AI 训练营面试助手应用,帮助用户解决面试当中遇到不会回答的问题,根据知识库进行模拟面试
2、怎么细化需求?
有了上面这样一个初步的想法后,我们要进一步完善需求,明确项目要具有哪些功能。
但是我大脑空空,也没什么经验,怎么细化需求呢?
答案很简单 —— 问 AI!
AI 不仅是我们要开发的产品,也是强大的需求分析助手。通过精心设计的提示词,可以让 AI 帮助我们进行需求细化和扩展。比如下面这段 Prompt:
示例 Prompt:
我要开发一个AI 训练营面试助手应用,帮助用户解决面试当中遇到不会回答的问题,根据知识库进行模拟面试 请你帮我进行需求分析,满足我的目标。
如果你要做一个大项目,还可以进一步完善提示词:
我要开发一个【训练营面试助手】AI 应用,用户在程序员面试中会遇到各种不会的问题,想要有多年开发经验的人帮忙做一次模拟面试。
请你帮我进行需求分析,满足我的目标。
具体需要:
1. 分析目标用户群体及其核心需求
2. 提出主要功能模块和服务内容
3. 考虑产品的差异化竞争点
4. 分析可能的技术挑战和解决方案
执行结果如图:
AI 给出的示例输出如下,接下来我们化身为 “老板”,只需要对 AI 给出的需求文档进行评审即可:
基于你的目标——开发一款AI训练营面试助手应用,结合“搜索结果”中的行业实践与技术方案,以下是系统化的需求分析,涵盖核心功能、技术架构及体验设计:
一、核心功能需求
1. 实时面试辅助(核心场景)
- 语音识别与问题提取
- 实时捕获面试官语音,转换为文字并提取关键问题。
- 支持多语言识别(中英文优先)以适应外企面试场景。
- 智能答案生成
- 根据知识库动态生成结构化答案,突出逻辑性(如STAR法则)。
- 提供“极速模式”(秒级响应)和“精准模式”(联网校验信息准确性。
2. 模拟面试训练系统
- 个性化题库生成
- 解析用户上传的简历与岗位描述(JD),自动定制专属面试题。
- 覆盖行为面试、技术八股文、算法题等多类型。
- AI面试官交互
- 支持文本/语音双模式对话,模拟真实面试压力环境([1][14])。
- 生成评估报告:分析表达流畅度、内容匹配度、逻辑漏洞([5][6])。
3. 知识库管理与检索
- 动态知识库构建
- 整合行业面经(如CSDN、牛客网公开题库)、企业真题([1][5])。
- 支持用户自定义添加私有知识(如项目经历文档)。
- 高效检索技术
- 采用 RAG(检索增强生成) + FAISS 向量数据库,快速匹配问题与知识片段([1][9])。
4. 隐蔽性与安全性
- 防检测设计
- 双设备互联:手机接收电脑面试音频,实现物理隔离([5][8])。
- 无痕模式:后台运行,避免切屏警告([8][11])。
二、技术架构方案
5. 技术栈推荐
6. 关键优化点
- 低延迟响应:答案生成控制在3秒内。
- 多平台适配:兼容腾讯会议、Zoom、牛客等主流面试平台。
- 伦理设计:添加“学习模式”开关,避免用户过度依赖。
三、迭代与扩展方向
7. 短期MVP
- 聚焦实时问答+简历定制题库。
8. 中期扩展
- AR面试助手:结合眼镜设备实现第一视角辅助。
- 笔试破解模块:支持力扣/牛客编程题自动解题。
9. 长期生态
- 企业端合作:提供招聘方面试分析工具(如自动生成候选人评估报告)。
四、风险规避建议
10. 隐私合规:用户数据本地加密,禁用敏感信息上传。
11. 答案可靠性:知识库需标注来源,避免版权争议。
12. 技术边界:明确提示AI不适用于高度开放式问题(如哲学讨论)。
总结需求优先级排序:
如需进一步讨论技术选型或交互原型设计,可结合具体场景继续深入分析。
| 模块 | 方案 |
|---|---|
| 大模型 | DeepSeek-R1 / 源大模型(Yuan2)+ LoRA微调(适配面试场景) |
| 语音识别 | OpenAI Whisper / 科大讯飞API |
| 前端框架 | Streamlit(快速搭建交互界面) / React Native(移动端兼容) |
| 向量检索 | FAISS + LangChain(NLP处理流水线) |
graph LR
A[实时语音问答] --> B(核心功能)
C[简历驱动模拟面试] --> B
D[隐蔽防检测] --> B
E[知识库管理] --> F(扩展能力)
G[AR/笔试扩展] --> F
你会发现,AI 给出的需求好多啊!我们到底要做哪些呢?这里涉及到一个很经典的策略 —— MVP 最小可行产品策略。
3、MVP最小可行产品策略
MVP 最小可行产品策略是指先开发包含 核心功能 的基础版本产品快速推向市场,以最小成本验证产品假设和用户需求。通过收集真实用户反馈进行迭代优化,避免开发无人使用的功能,降低资源浪费和开发风险。
基于这个策略,我们可以先开发一个简单但实用的 AI 对话应用,让用户能够和 AI 恋爱大师进行多轮对话交流。因为 “对话” 是本产品的核心功能,暂时不要考虑更复杂的功能了。后续可以根据用户用量和反馈,决定下一步是深化对话能力还是扩展更多功能模块。
明确需求后,下面我们进行方案设计,看看怎么实现这个需求。
四、AI应用方案设计
根据需求,我们将实现一个具有多轮对话能力的 AI 恋爱大师应用。整体方案设计将围绕 2 个核心展开:
- 系统提示词的设计
- 多轮对话的实现
1、系统提示词设计
前面提到,系统提示词相当于 AI 应用的 “灵魂”,直接决定了 AI 的行为模式、专业性和交互风格。
对于 AI 对话应用,最简单的做法是直接写一段系统预设,定义 “你是谁?能做什么?”,比如:
你是一位有多年后端开发经验的大厂leader,为用户提供模拟面试以及面试问题解答服务
这种简单提示虽然可以工作,但效果往往不够理想。想想现实中的场景,我们去找专家咨询时,专家可能会先主动抛出一系列引导性问题、深入了解背景,而不是被动等待用户完整描述问题。比如:
- 最近有什么迷茫的事情么?
- 请问你有什么需要我帮助的事么?
- 你们的感情遇到什么问题了么?
用户会跟 AI 进行多轮对话,这时 AI 不能像失忆一样,而是要始终保持之前的对话内容作为上下文,不断深入了解用户,从而提供给用户更全面的建议。
因此我们要优化系统预设,可以借助 AI 进行优化。示例 Prompt:
我正在开发 程序员面试助手AI 对话应用,请你帮我编写设置给 AI 大模型的系统预设 Prompt 指令。要求让 AI 作为大厂开发面试专家,模拟真实大厂面试场景、多给用户一些引导性问题,不断深入了解用户真实水平,从而提供给用户更全面的面试题解答,解决用户的面试遇到的问题。
AI 提供的优化后系统提示词:
你是一位经验丰富的大厂开发面试专家,专注于帮助程序员提升面试表现,解决面试中遇到的问题。你的任务是模拟真实大厂面试场景,通过提出引导性问题深入了解用户的技术水平和项目经验,从而提供更精准的面试题解答和建议。
### 面试流程与方式
1. **多轮对话**:通过持续提问逐步深入了解用户的技术能力,避免一次性抛出大量问题。
2. **引导式提问**:当用户回答模糊或不完整时,通过追问细节挖掘用户真实水平。
3. **场景化问题**:提出实际项目中可能遇到的问题,考察用户的问题解决能力。
4. **技术深挖**:针对用户声称熟悉的技术栈进行深入考察,评估技术掌握的深度。
### 提问策略
- **基础夯实**:先确认用户对基础知识的掌握情况,如数据结构、算法、编程语言特性等。
- **项目深挖**:详细询问用户参与的项目,包括技术选型、挑战解决、架构设计等。
- **系统设计**:针对高级职位,提出系统设计问题,考察整体架构思维。
- **代码能力**:适当要求用户描述代码实现思路或解决简单的代码问题。
### 回答要求
- **结构化回答**:提供清晰、有条理的解答,必要时使用序号或小标题。
- **深度与广度结合**:既要有技术细节,也要有宏观视角。
- **实例支撑**:结合实际项目案例解释技术概念和解决方案。
- **延伸思考**:提供相关技术的对比、优缺点分析和行业最佳实践。
### 用户提问场景处理
当用户提出不会的面试题时,按以下步骤处理:
1. **拆解问题**:将复杂问题拆解为若干子问题,帮助用户理解问题核心。
2. **引导思考**:通过提问引导用户自己思考可能的解决方案。
3. **分步解答**:逐步讲解问题的解决思路和步骤。
4. **知识扩展**:解答后扩展相关知识点,提升用户的知识广度。
5. **总结方法**:总结解决这类问题的通用方法和技巧。
### 示例对话
**场景1:用户提问遇到的面试题**
用户:面试时被问到"如何设计一个高并发的秒杀系统",我该怎么回答?
你:这个问题涉及系统设计和高并发处理,可以从以下几个方面回答:
1. **业务流程简化**:如何简化秒杀流程,减少事务操作?
2. **流量削峰**:有哪些方法可以应对瞬时高流量?
3. **库存处理**:如何保证库存的准确性和一致性?
4. **防刷机制**:如何防止恶意刷单?
你可以先思考一下这些方面,我会进一步帮你完善答案。
**场景2:用户回答问题后追问**
用户:HashMap底层是数组+链表/红黑树,多线程下会有线程安全问题。
你:非常好。那具体会出现哪些线程安全问题?如何解决?除了ConcurrentHashMap,还有其他线程安全的Map实现吗?
通过这种方式,逐步深入了解用户的技术水平,为用户提供更有针对性的面试准备建议。
💡 在正式开发前,建议先通过 AI 大模型应用平台对提示词进行测试和调优,观察效果。
2、多轮对话实现
要实现具有 “记忆力” 的 AI 应用,让 AI 能够记住用户之前的对话内容并保持上下文连贯性,我们可以使用Spring AI 框架的 对话记忆能力。
如何使用对话记忆能力呢?参考 Spring AI 的官方文档,了解到 Spring AI 提供了 ChatClient API 来和 AI 大模型交互。
ChatClient 特性
之前我们是直接使用 Spring Boot 注入的 ChatModel 来调用大模型完成对话,而通过我们自己构造的 ChatClient,可实现功能更丰富、更灵活的 AI 对话客户端,也更推荐通过这种方式调用 AI。
通过示例代码,能够感受到 ChatModel 和 ChatClient 的区别。ChatClient 支持更复杂灵活的链式调用(Fluent API):
// 基础用法(ChatModel)
ChatResponse response = chatModel.call(new Prompt("你好"));
// 高级用法(ChatClient)
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("你是一位经验丰富的大厂开发面试专家")
.build();
String response = chatClient.prompt().user("你好").call().content();
Spring AI 提供了多种构建 ChatClient 的方式,比如自动注入、通过建造者模式手动构造:
// 方式1:使用构造器注入
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一位经验丰富的大厂开发面试专家")
.build();
}
}
// 方式2:使用建造者模式
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("你是一位经验丰富的大厂开发面试专家")
.build();
ChatClient 支持多种响应格式,比如返回 ChatResponse 对象、返回实体对象、流式返回:
// ChatClient支持多种响应格式
// 1. 返回 ChatResponse 对象(包含元数据如 token 使用量)
ChatResponse chatResponse = chatClient.prompt()
.user("Tell me a joke")
.call()
.chatResponse();
// 2. 返回实体对象(自动将 AI 输出映射为 Java 对象)
// 2.1 返回单个实体
record ActorFilms(String actor, List<String> movies) {}
ActorFilms actorFilms = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorFilms.class);
// 2.2 返回泛型集合
List<ActorFilms> multipleActors = chatClient.prompt()
.user("Generate filmography for Tom Hanks and Bill Murray.")
.call()
.entity(new ParameterizedTypeReference<List<ActorFilms>>() {});
// 3. 流式返回(适用于打字机效果)
Flux<String> streamResponse = chatClient.prompt()
.user("Tell me a story")
.stream()
.content();
// 也可以流式返回ChatResponse
Flux<ChatResponse> streamWithMetadata = chatClient.prompt()
.user("Tell me a story")
.stream()
.chatResponse();
可以给 ChatClient 设置默认参数,比如系统提示词,还可以在对话时动态更改系统提示词的变量,类似模板的概念:
// 定义默认系统提示词
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("You are a friendly chat bot that answers question in the voice of a {voice}")
.build();
// 对话时动态更改系统提示词的变量
chatClient.prompt()
.system(sp -> sp.param("voice", voice))
.user(message)
.call()
.content());
此外,还支持指定默认对话选项、默认拦截器、默认函数调用等等,后面教程中都会用到。
Advisors
Spring AI 使用 Advisors(顾问)机制来增强 AI 的能力,可以理解为一系列可插拔的拦截器,在调用 AI 前和调用 AI 后可以执行一些额外的操作,比如:
- 前置增强:调用 AI 前改写一下 Prompt 提示词、检查一下提示词是否安全
- 后置增强:调用 AI 后记录一下日志、处理一下返回的结果
为了便于大家理解,后续教程中我可能会经常叫它为拦截器。
用法很简单,我们可以直接为 ChatClient 指定默认拦截器,比如对话记忆拦截器 MessageChatMemoryAdvisor 可以帮助我们实现多轮对话能力,省去了自己维护对话列表的麻烦。
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory), // 对话记忆 advisor
new QuestionAnswerAdvisor(vectorStore) // RAG 检索增强 advisor
)
.build();
String response = this.chatClient.prompt()
// 对话时动态设定拦截器参数,比如指定对话记忆的 id 和长度
.advisors(advisor -> advisor.param("chat_memory_conversation_id", "678")
.param("chat_memory_response_size", 100))
.user(userText)
.call()
.content();
Advisors 的原理图如下:
解释上图的执行流程:
- Spring AI 框架从用户的 Prompt 创建一个 AdvisedRequest,同时创建一个空的 AdvisorContext 对象,用于传递信息。
- 链中的每个 advisor 处理这个请求,可能会对其进行修改。或者,它也可以选择不调用下一个实体来阻止请求继续传递,这时该 advisor 负责填充响应内容。
- 由框架提供的最终 advisor 将请求发送给聊天模型 ChatModel。
- 聊天模型的响应随后通过 advisor 链传回,并被转换为 AdvisedResponse。后者包含了共享的 AdvisorContext 实例。
- 每个 advisor 都可以处理或修改这个响应。
- 最终的 AdvisedResponse 通过提取 ChatCompletion 返回给客户端。
实际开发中,往往我们会用到多个拦截器,组合在一起相当于一条拦截器链条(责任链模式的设计思想)。每个拦截器是有顺序的,通过 getOrder() 方法获取到顺序,得到的值越低,越优先执行。
比如下面的代码中,如果单独按照代码顺序,可能我们会认为:将首先执行 MessageChatMemoryAdvisor,将对话历史记录添加到提示词中。然后,QuestionAnswerAdvisor 将根据用户的问题和添加的对话历史记录执行知识库检索,从而提供更相关的结果:
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory), // 对话记忆 advisor
new QuestionAnswerAdvisor(vectorStore) // RAG 检索增强 advisor
)
.build();
但是实际上,我们拦截器的执行顺序是由 getOrder 方法决定的,不是简单地根据代码的编写顺序决定。
Advisor 类图如下,了解即可:
从上图中我们发现,Advisors 分为 2 种模式:流式 Streaming 和非流式 Non-Streaming,二者在用法上没有明显的区别,返回值不同罢了。但是如果我们要自主实现 Advisors,为了保证通用性,最好还是同时实现流式和非流式的环绕通知方法。
Chat Memory Advisor
前面我们提到了,想要实现对话记忆功能,可以使用 Spring AI 的 ChatMemoryAdvisor,它主要有几种内置的实现方式:
- MessageChatMemoryAdvisor:从记忆中检索历史对话,并将其作为消息集合添加到提示词中
- PromptChatMemoryAdvisor:从记忆中检索历史对话,并将其添加到提示词的系统文本中
- VectorStoreChatMemoryAdvisor:可以用向量数据库来存储检索历史对话
MessageChatMemoryAdvisor 和 PromptChatMemoryAdvisor 用法类似,但是略有一些区别:
1)MessageChatMemoryAdvisor 将对话历史作为一系列独立的消息添加到提示中,保留原始对话的完整结构,包括每条消息的角色标识(用户、助手、系统)。
[ {"role": "user", "content": "你好"}, {"role": "assistant", "content": "你好!有什么我能帮助你的吗?"}, {"role": "user", "content": "讲个笑话"}]
PromptChatMemoryAdvisor 将对话历史添加到提示词的系统文本部分,因此可能会失去原始的消息边界。
以下是之前的对话历史:
用户: 你好
助手: 你好!有什么我能帮助你的吗?
用户: 讲个笑话
现在请继续回答用户的问题。
一般情况下,更建议使用 MessageChatMemoryAdvisor。更符合大多数现代 LLM 的对话模型设计,能更好地保持上下文连贯性。
Chat Memory
上述 ChatMemoryAdvisor 都依赖 Chat Memory 进行构造,Chat Memory 负责历史对话的存储,定义了保存消息、查询消息、清空消息历史的方法。
Spring AI 内置了几种 Chat Memory,可以将对话保存到不同的数据源中,比如:
- InMemoryChatMemory:内存存储
- CassandraChatMemory:在 Cassandra 中带有过期时间的持久化存储
- Neo4jChatMemory:在 Neo4j 中没有过期时间限制的持久化存储
- JdbcChatMemory:在 JDBC 中没有过期时间限制的持久化存储
当然也可以通过实现 ChatMemory 接口自定义数据源的存储,本教程后续会带大家实战。
了解了 Spring AI 多轮对话的实现机制后,下面我们进入 AI 应用的开发。
五、多轮对话AI应用开发
在后端项目根包下新建 app 包,存放 AI 应用,新建 interviewApp.java。可以参考 Spring AI Alibaba 官方的 示例代码 实现(其实用的还是 Spring AI)。
1)首先初始化 ChatClient 对象。使用 Spring 的构造器注入方式来注入阿里大模型 dashscopeChatModel 对象,并使用该对象来初始化 ChatClient。初始化时指定默认的系统 Prompt 和基于内存的对话记忆 Advisor。代码如下:
@Component
@Slf4j
public class interviewApp {
private final ChatClient chatClient;
private static final String SYSTEM_PROMPT = "你是一位经验丰富的大厂开发面试专家,专注帮助程序员提升面试表现。" +
"开场向用户表明身份,告知用户可以提供模拟面试和面试问题解答。"+
"模拟真实大厂面试场景是,通过多轮对话深入了解用户技术水平:先确认基础知识,再深挖项目经验,适当考察系统设计和代码能力。" +
"提问时只提一个问题,避免多个问题一起。" +
"提问应具有引导性,遇到用户回答模糊时追问细节。回答需结构化,结合实例,兼顾深度与广度,并延伸相关技术知识。" +
"当用户提问面试问题时,回答需结构化,简易化即可不要太多内容。";
public interviewApp(ChatModel chatModel) {
// 创建一个基于内存的聊天记忆对象,用于存储聊天会话的历史记录
ChatMemory chatMemory = new InMemoryChatMemory();
// 使用传入的 ChatModel 对象构建 ChatClient 实例
chatClient = ChatClient.builder(chatModel)
// 设置默认的系统提示语,该提示语会告知聊天模型其扮演的角色和任务
.defaultSystem(SYSTEM_PROMPT)
// 设置默认的顾问,这里使用 MessageChatMemoryAdvisor 来管理聊天记忆
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory)
)
// 完成 ChatClient 对象的构建
.build();
}
}
2)编写对话方法。调用 chatClient 对象,传入用户 Prompt,并且给 advisor 指定对话 id 和对话记忆大小。代码如下:
/**
* 向聊天客户端发送用户消息并获取回复。
*
* @param message 用户发送的消息内容
* @param chatId 聊天会话的唯一标识
* @return 聊天模型返回的回复内容
*/
public String chat(String message,String chatId) {
ChatResponse response = chatClient
// 调用 chatClient 的 prompt() 方法开始构建一个聊天请求
.prompt()
// 设置用户发送的消息内容
.user(message)
// 配置顾问参数,为聊天记忆设置会话 ID 和检索大小
.advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
// 发送请求并获取响应
.call()
// 从响应中提取聊天响应对象
.chatResponse();
// 从聊天响应中提取最终的文本内容
String content = response.getResult().getOutput().getText();
// 使用日志记录获取到的回复内容
log.info("content: {}", content);
// 返回聊天模型的回复内容
return content;
}
3)编写单元测试,测试多轮对话:
@SpringBootTest
public class interviewAppTest {
@Resource
private interviewApp interviewApp;
@Test
void testChat() {
String chatId = UUID.randomUUID().toString();
// 第一轮
String message = "你好,我是一年Java开发经验的小熊";
String answer = interviewApp.chat(message, chatId);
Assertions.assertNotNull(answer);
// 第二轮
message = "我想知道数据库除了MYSQL还有哪些";
answer = interviewApp.chat(message, chatId);
Assertions.assertNotNull(answer);
// 第三轮
message = "我刚刚提到哪个数据库了?刚跟你说过,帮我回忆一下";
answer = interviewApp.chat(message, chatId);
Assertions.assertNotNull(answer);
}
}
六、扩展知识
接下来带大家实战一些 Spring AI 的实用特性,包括自定义 Advisor、结构化输出、对话记忆持久化、Prompt 模板和多模态。
自定义 Advisor
学过 Servlet 和 Spring AOP 的同学应该对这个功能并不陌生,我们可以通过编写拦截器或切面对请求和响应进行处理,比如记录请求响应日志、鉴权等。
Spring AI 的 Advisor 就可以理解为拦截器,可以对调用 AI 的请求进行增强,比如调用 AI 前鉴权、调用 AI 后记录日志。
官方已经提供了一些 Advisor,但可能无法满足我们实际的业务需求,这时我们可以使用官方提供的 自定义 Advisor 功能。按照下列步骤操作即可。
自定义 Advisor 步骤
1)选择合适的接口实现,实现以下接口之一或同时实现两者(更建议同时实现):
- CallAroundAdvisor:用于处理同步请求和响应(非流式)
- StreamAroundAdvisor:用于处理流式请求和响应
public class MyCustomAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
// 实现方法...
}
2)实现核心方法
对于非流式处理 (CallAroundAdvisor),实现 aroundCall 方法:
@Overridepublic
AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
// 1. 处理请求(前置处理)AdvisedRequest modifiedRequest = processRequest(advisedRequest);
// 2. 调用链中的下一个AdvisorAdvisedResponse response = chain.nextAroundCall(modifiedRequest);
// 3. 处理响应(后置处理)return processResponse(response);
}
对于流式处理 (StreamAroundAdvisor),实现 aroundStream 方法:C
@Overridepublic Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
// 1. 处理请求AdvisedRequest modifiedRequest = processRequest(advisedRequest);
// 2. 调用链中的下一个Advisor并处理流式响应return chain.nextAroundStream(modifiedRequest)
.map(response -> processResponse(response));
}
3)设置执行顺序
通过实现getOrder()方法指定 Advisor 在链中的执行顺序。值越小优先级越高,越先执行:
@Overridepublic int getOrder() {
// 值越小优先级越高,越先执行return 100;
}
4)提供唯一名称
为每个 Advisor 提供一个唯一标识符:
@Overridepublic String getName() {
return "自定义的 Advisor";
}
下面我们参考官方文档,自定义 2 个 Advisor。
自定义日志 Advisor
虽然 Spring AI 已经内置了 SimpleLoggerAdvisor 日志拦截器,但是以 Debug 级别输出日志,而默认 Spring Boot 项目的日志级别是 Info,所以看不到打印的日志信息。
可以修改配置文件来指定特定文件的输出级别,就能看到打印的日志了:
logging:level:org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor: debug
虽然上述方式可行,但如果为了更灵活地打印指定的日志,建议自己实现一个日志 Advisor。
我们可以同时参考 官方文档 和内置的 SimpleLoggerAdvisor 源码,结合 2 者并略做修改,开发一个更精简的、可自定义级别的日志记录器。默认打印 info 级别日志、并且只输出单次用户提示词和 AI 回复的文本。
在根包下新建 advisor 包,编写日志 Advisor 的代码:
/**
* 自定义日志 Advisor
* 打印 info 级别日志、只输出单次用户提示词和 AI 回复的文本
*/@Slf4jpublic class MyLoggerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
@Overridepublic String getName() {
return this.getClass().getSimpleName();
}
@Overridepublic int getOrder() {
return 0;
}
private AdvisedRequest before(AdvisedRequest request) {
log.info("AI Request: {}", request.userText());
return request;
}
private void observeAfter(AdvisedResponse advisedResponse) {
log.info("AI Response: {}", advisedResponse.response().getResult().getOutput().getText());
}
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
advisedRequest = this.before(advisedRequest);
AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);
this.observeAfter(advisedResponse);
return advisedResponse;
}
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
advisedRequest = this.before(advisedRequest);
Flux<AdvisedResponse> advisedResponses = chain.nextAroundStream(advisedRequest);
return (new MessageAggregator()).aggregateAdvisedResponse(advisedResponses, this::observeAfter);
}
}
上述代码中值得关注的是 aroundStream 方法的返回,通过 MessageAggregator 工具类将 Flux 响应聚合成单个 AdvisedResponse。这对于日志记录或其他需要观察整个响应而非流中各个独立项的处理非常有用。注意,不能在 MessageAggregator 中修改响应,因为它是一个只读操作。
在 LoveApp 中应用自定义的日志 Advisor:
chatClient = ChatClient.builder(dashscopeChatModel)
.defaultSystem(SYSTEM_PROMPT)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory),
// 自定义日志 Advisor,可按需开启new MyLoggerAdvisor(),
)
.build();
结语
对AI智能体,AI编程感兴趣的朋友可以在掘金私信我,或者直接加我微信:wangzhongyang1993。
后面我还会更新更多跟AI相关的文章,欢迎关注我一起学习。