二哥项目资料

求职派:深入拆解 Spring AI 提示词机制,附简历写法

正文 21,437 字 #04 原文
本文目录 · 18 节

星球:Java进阶之路&二哥编程星球 | 专栏:求职派 OpenClaw 架构 | 作者:沉默王二 | 发布:2026-07-04

求职派:深入拆解 Spring AI 提示词机制,附简历写法

求职派:深入拆解 Spring AI 提示词机制,附简历写法

之前我们跑通了第一个 Spring AI 工程,大模型能正常回话了。

但把用户文本原封不动丢给模型,输出往往并不可靠。提示词是解决这些问题的关键钥匙,也是整个 Agent 系统的根基。

本篇内容第一版的作者:星球嘉宾灰灰。二哥做了二期的优化和迭代。

Spring AI 2.0 把提示词抽象成了 Prompt、Message、PromptTemplate 三层结构。包括身份注入处理、上下文管理、多模态消息、模型自动切换等。

图片.png

这篇内容将从基础概念出发,逐步串联到求职派的真实源码,让大家搞清楚提示词是如何从 API 概念到生产落地。

01、Prompt 不只是一段文本

很多教程把提示词等同于“一段文本”。

在大模型的 HTTP API 层面,请求体里确实有个 messages 数组。但 Spring AI 把它抽象成了一个更丰富的对象,包含消息列表和调用参数。

Java

public class Prompt implements ModelRequest> {
    private final List messages;
    private ChatOptions chatOptions;
}

messages 是一个消息列表,每条消息绑定一个角色。chatOptions 控制模型调用的行为参数,比如温度、模型名称。

图片.png

这个设计和 OpenAI API 的 messages 数组直接对应,Spring AI 用 Java 的 class 对象把 JSON 结构封装了一层。

创建 Prompt 最简单的方式是 new Prompt("给我讲个笑话"),Spring AI 自动把文本包装成一条用户消息。

但真实项目里几乎不会这么用,因为缺少系统指令,大模型不知道自己该扮演什么角色,输出质量全靠运气。如果想指定调用参数,就要传入第二个参数 ChatOptions。

Java

Prompt prompt = new Prompt(message,
    ZhiPuAiChatOptions.builder()
        .model("glm-5.2").temperature(0.7).build());

model 指定调用哪个模型,temperature 控制输出的随机性。

这两个参数几乎在每次调用时都会用到。搞清楚了 Prompt 的结构,下一个问题自然就来了,消息列表里的每条消息,角色是怎么分工的。

02、四种消息角色的分工

每条消息都有一个类型字段,和大模型定义的角色一一映射。

图片.png

  • system 负责系统指令,告诉大模型“该扮演什么角色、遵守什么规则”。

  • user 承载用户输入,文本和图片都绑定到 user

  • assistant 标记模型生成的内容,由框架自动管理

  • tool 对应函数调用的返回结果,Agent 的 ReAct 循环里会用到

四个角色里,我们直接操控的是 system 和 user。来看一个直观的例子,给大模型设定一个“儿童聊天助手”身份。

Java

Prompt prompt = new Prompt(
    List.of(new SystemMessage("你是专注于给3-5岁儿童聊天的助手"),
            new UserMessage(message)),
    ZhiPuAiChatOptions.builder()
        .model("glm-5.2").temperature(0.7).build());

加上 SystemMessage 后,同样问“讲个笑话”,输出明显更适合小朋友。

图片.png

temperature 设为 0.7 意味着要平衡确定性与多样性。

接近 0 时输出趋于保守,接近 1 时输出更有创造性但可能跑偏。实际业务场景里,问答类建议 0.3 左右,创意类可以调到 0.8。

求职派的业务 Agent 调用器在初始化时,会把用户画像和 Agent 指令拼接成一条系统消息。

Java

String identity = identityAgent.buildSoulPrompt(userId);
if (StringUtils.isNotBlank(identity)) {
    sys = buildSystemPrompt(identity + "\n\n" + systemPrompt);
}

这段逻辑先从身份系统拿到用户画像(教育背景、技能、求职偏好),再和 Agent 自身的系统指令拼在一起。

图片.png

同一个岗位的推荐 Agent,面对应届生和三年经验的开发者,会给出完全不同的推荐结果。系统消息不是写死的,而是跟着用户动态变化。

多模态场景下,用户消息还能携带图片和文件。

求职派从微信/钉钉/飞书收到的消息经过渠道适配后,文本和图片通过 UserMessage.builder() 统一装进一条用户消息(代码位置 core/.../BizAgentLlmCaller.java,第 203-209 行)。图片以 Media 对象的形式挂在 UserMessage 上,支持字节数组和文件路径两种来源。

Media 附件,决定了后面调文本模型还是视觉模型。

03、提示词模板

当提示词里有动态内容时,硬编码拼接会变得不可维护。Spring AI 提供了模板机制,用 {变量名} 做占位符。

Java

PromptTemplate tpl = new PromptTemplate(
    "你来扮演{personality}的{aiRole}, 我来扮演{myRole}");
Prompt prompt = tpl.create(Map.of(
    "personality", "温柔", "aiRole", "女朋友", "myRole", "男朋友"));

传入不同参数,大模型的表现截然不同。

图片.png

默认 create() 生成的是用户消息类型。

如果要生成系统提示词,换成 SystemPromptTemplate 调 createMessage() 即可。

在求职派里,PromptTemplate 并不是主力。系统提示词会 把 .md 文件当模板,用 String.replace 做变量替换。

以身份提取器为例,启动时通过 @Value 注入 classpath 下的 .md 文件,调用时直接 replace 替换变量。

Java

@Value("classpath:/prompts/user-identity-extraction-prompt.md")
Resource promptResource;
this.promptTemplate = promptResource.getContentAsString(UTF_8);
// 调用时
String prompt = promptTemplate
    .replace("{current_soul}", existingIdentity)
    .replace("{conversation_history}", conversationText);

为什么不用 PromptTemplate?

看一眼身份提取器的模板文件就明白了。

这个 .md 文件接近 200 行,前面定义角色(“你是一个专业的用户画像分析助手”),中间规定输出格式(Markdown 结构,包含求职偏好、教育背景、技能清单等 10 多个字段),后面给出 2 个完整的 Few-shot 示例教模型怎么做增量更新。

整个模板里只有 2 个变量占位符: {current_soul} 和 {conversation_history}。

这种“变量极少但模板极长”的场景,PromptTemplate 的模板引擎优势发挥不出来,反而是 .md 文件 + String.replace 最直接。

图片.png

模板放在 resources 目录下跟随代码一起进行版本管理,产品经理改一个输出字段不需要动 Java 代码,改完 .md 文件重启就生效。

求职派目前有 5 个以上的提示词模板文件分布在不同 Agent 模块的 resources/prompts/ 目录下,涵盖身份提取、灵魂画像、岗位信息抽取、会话摘要等场景。

图片.png

每个 Agent 的提示词独立管理,互不干扰。

提示词外置到资源文件,是 Spring AI 项目从 demo 走向生产几乎必经的一步。

04、LLM 调用封装

直接在业务代码里写 chatModel.call(prompt) 有问题吗?

有。

至少三个,每个 Agent 都要自己处理模型选择、上下文记忆管理、多模态消息构建。求职派为此抽象了一层调用器。

Java

public interface LlmCaller {
    String call(UserConversationInfo user, Prompt prompt);
    Flux stream(UserConversationInfo user, Prompt prompt);
}

三个实现类层层递进。

  • 轻量调用器,由调用者自己维护上下文,不带记忆也不注册工具。适合身份提取、会话摘要这类一次性调用,用完即走

  • 业务 Agent 调用器,加了 ChatMemory 做对话持久化,接入自定义的 ReAct 推理循环,工具调用经过中间件拦截。大部分业务 Agent(岗位抓取、身份收集)用这一层

  • 用户偏好调用器,在业务 Agent 基础上自动读取用户画像注入系统消息,同时注册文件系统、Shell、MCP、网页抓取等十余种工具。求职推荐 Agent 用的就是这个

每一层在上一层的基础上只增加必要的能力,业务代码按需选用,不必为简单场景付出复杂场景的开销。

图片.png

这里面有这样一个巧妙的设计,模型自动切换。当用户消息包含图片时,系统自动从文本模型切到视觉模型。

Java

if (prompt.getUserMessages().stream()
    .anyMatch(m -> !CollectionUtils.isEmpty(m.getMedia()))) {
    model = ModelConfig.ModelType.VISION;
}

业务 Agent 完全不用关心“用户发了图片该调哪个模型”,调用器在底层自动判断。

这种把基础设施决策下沉到公共层的做法,在多 Agent 系统里能显著降低每个 Agent 的实现复杂度。

05、历史消息怎么管理?

多轮对话场景下,Prompt 的消息列表会不断增长,最终超出大模型的上下文窗口。

求职派用智能窗口管理器解决了这个问题,分三步。

图片.png

第一步,过滤无用消息。正则匹配纯确认类回复,“好的”“嗯”“OK”“收到”这些不携带实际信息的消息直接过滤。

Java

Pattern.compile(
    "^(好的|收到|嗯|哦|好|知道了|明白了|ok|OK|yes)$",
    Pattern.CASE_INSENSITIVE);

只过滤用户消息,模型回复即使很短也保留,因为短回复可能是关键确认。

第二步,token 估算与裁剪。中文字符按每字 2 个 token 计算,英文单词按 1.3 个 token 计算,加上每条消息 10 个 token 的结构开销。

超出预算时从最旧的消息开始删除,但保留最近若干条不动。

第三步,总条数兜底。即使 token 没超,消息条数超过上限也会裁剪。

Spring AI 自带的 MessageWindowChatMemory 只支持按条数裁剪,不区分消息的信息密度。

一条“嗯”和一条 500 字的技术讨论在它眼里是等权的。

求职派在这个基础上做了两个改进,基于 token 的裁剪让预算控制更精确,无用消息过滤把有限的窗口留给真正有价值的内容。尤其在中文场景下,一个汉字约等于 2 个 token,如果不做区分估算,纯按英文假设计算会严重低估实际 token 消耗。

当对话更长时,求职派还有一层会话摘要机制作为补充。

超过一定轮数的历史消息会被 LLM 压缩成一段摘要,以一条新的消息注入回上下文,这样既不丢失早期对话的核心信息,又把 token 占用压到原来的十分之一左右。

窗口管理和会话摘要配合使用,保证了求职派在长对话场景下的稳定表现。

从提示词构建到上下文管理,整条线环环相扣——Prompt 承载消息和参数,Message 的角色决定信息的定位,模板让提示词可复用可维护,调用器在底层统一处理模型选择和身份注入,窗口管理器则确保历史消息不会撑爆上下文。

提示词是大模型交互的起点,也是最容易被低估的环节。

【把提示词当作工程问题来对待,大模型才会真正按我们想要的方式工作。】

06、如何把求职派写到简历上?

项目名称:求职派(JobClaw)多 Agent 求职系统

项目简介:基于 Spring AI 2.0 的多 Agent 求职平台,支持微信/钉钉/飞书多渠道接入,通过意图识别、Agent 路由、用户画像和上下文记忆,为用户提供身份采集、岗位抓取、岗位推荐和求职问答服务。

技术栈:Java 21、Spring Boot 4.0、Spring AI 2.0、LangGraph4J、Reactor、YAML、H2/MySQL

核心职责:

  • 基于 Spring AI 的 Prompt 和 ChatClient 抽象,封装 LLM 调用器(轻量/业务/用户偏好),统一处理模型选择、流式响应、会话记忆持久化和工具传参。

  • 将 9 个提示词模板外置为 Markdown 资源文件,覆盖身份提取、灵魂画像、岗位抽取、会话摘要等场景,提示词与 Java 代码解耦,支持多 Agent 独立迭代,产品侧改模板不需要动业务代码。

  • 实现智能上下文窗口管理,通过低效消息过滤、中英文 token 估算将多轮对话历史控制在预算内;支持超长对话触发异步摘要压缩,每 10 轮刷新一次,旧消息压缩为 100 字以内的摘要并归档。

  • 设计用户身份注入机制,按系统规则、性格画像、基础信息、求职偏好四层维护用户档案,自动从对话中增量提取并拼接到系统提示词,同一个推荐 Agent 面对不同用户给出差异化推荐结果。

  • 基于自定义 ReAct 替换 Spring AI 内置的自动工具执行,注册文件系统、Shell、MCP、网页抓取等十余种工具,并在推理-行动中插入中间件 Hook 钩子,支持 Function Calling 的日志记录和行为审计。

之前我们跑通了第一个 Spring AI 工程,大模型能正常回话了。

但把用户文本原封不动丢给模型,输出往往并不可靠。提示词是解决这些问题的关键钥匙,也是整个 Agent 系统的根基。

本篇内容第一版的作者:星球嘉宾灰灰。二哥做了二期的优化和迭代。

Spring AI 2.0 把提示词抽象成了 Prompt、Message、PromptTemplate 三层结构。包括身份注入处理、上下文管理、多模态消息、模型自动切换等。

图片.png

这篇内容将从基础概念出发,逐步串联到求职派的真实源码,让大家搞清楚提示词是如何从 API 概念到生产落地。

01、Prompt 不只是一段文本

很多教程把提示词等同于“一段文本”。

在大模型的 HTTP API 层面,请求体里确实有个 messages 数组。但 Spring AI 把它抽象成了一个更丰富的对象,包含消息列表和调用参数。

Java

public class Prompt implements ModelRequest> {
    private final List messages;
    private ChatOptions chatOptions;
}

messages 是一个消息列表,每条消息绑定一个角色。chatOptions 控制模型调用的行为参数,比如温度、模型名称。

图片.png

这个设计和 OpenAI API 的 messages 数组直接对应,Spring AI 用 Java 的 class 对象把 JSON 结构封装了一层。

创建 Prompt 最简单的方式是 new Prompt("给我讲个笑话"),Spring AI 自动把文本包装成一条用户消息。

但真实项目里几乎不会这么用,因为缺少系统指令,大模型不知道自己该扮演什么角色,输出质量全靠运气。如果想指定调用参数,就要传入第二个参数 ChatOptions。

Java

Prompt prompt = new Prompt(message,
    ZhiPuAiChatOptions.builder()
        .model("glm-5.2").temperature(0.7).build());

model 指定调用哪个模型,temperature 控制输出的随机性。

这两个参数几乎在每次调用时都会用到。搞清楚了 Prompt 的结构,下一个问题自然就来了,消息列表里的每条消息,角色是怎么分工的。

02、四种消息角色的分工

每条消息都有一个类型字段,和大模型定义的角色一一映射。

图片.png

  • system 负责系统指令,告诉大模型“该扮演什么角色、遵守什么规则”。

  • user 承载用户输入,文本和图片都绑定到 user

  • assistant 标记模型生成的内容,由框架自动管理

  • tool 对应函数调用的返回结果,Agent 的 ReAct 循环里会用到

四个角色里,我们直接操控的是 system 和 user。来看一个直观的例子,给大模型设定一个“儿童聊天助手”身份。

Java

Prompt prompt = new Prompt(
    List.of(new SystemMessage("你是专注于给3-5岁儿童聊天的助手"),
            new UserMessage(message)),
    ZhiPuAiChatOptions.builder()
        .model("glm-5.2").temperature(0.7).build());

加上 SystemMessage 后,同样问“讲个笑话”,输出明显更适合小朋友。

图片.png

temperature 设为 0.7 意味着要平衡确定性与多样性。

接近 0 时输出趋于保守,接近 1 时输出更有创造性但可能跑偏。实际业务场景里,问答类建议 0.3 左右,创意类可以调到 0.8。

求职派的业务 Agent 调用器在初始化时,会把用户画像和 Agent 指令拼接成一条系统消息。

Java

String identity = identityAgent.buildSoulPrompt(userId);
if (StringUtils.isNotBlank(identity)) {
    sys = buildSystemPrompt(identity + "\n\n" + systemPrompt);
}

这段逻辑先从身份系统拿到用户画像(教育背景、技能、求职偏好),再和 Agent 自身的系统指令拼在一起。

图片.png

同一个岗位的推荐 Agent,面对应届生和三年经验的开发者,会给出完全不同的推荐结果。系统消息不是写死的,而是跟着用户动态变化。

多模态场景下,用户消息还能携带图片和文件。

求职派从微信/钉钉/飞书收到的消息经过渠道适配后,文本和图片通过 UserMessage.builder() 统一装进一条用户消息(代码位置 core/.../BizAgentLlmCaller.java,第 203-209 行)。图片以 Media 对象的形式挂在 UserMessage 上,支持字节数组和文件路径两种来源。

Media 附件,决定了后面调文本模型还是视觉模型。

03、提示词模板

当提示词里有动态内容时,硬编码拼接会变得不可维护。Spring AI 提供了模板机制,用 {变量名} 做占位符。

Java

PromptTemplate tpl = new PromptTemplate(
    "你来扮演{personality}的{aiRole}, 我来扮演{myRole}");
Prompt prompt = tpl.create(Map.of(
    "personality", "温柔", "aiRole", "女朋友", "myRole", "男朋友"));

传入不同参数,大模型的表现截然不同。

图片.png

默认 create() 生成的是用户消息类型。

如果要生成系统提示词,换成 SystemPromptTemplate 调 createMessage() 即可。

在求职派里,PromptTemplate 并不是主力。系统提示词会 把 .md 文件当模板,用 String.replace 做变量替换。

以身份提取器为例,启动时通过 @Value 注入 classpath 下的 .md 文件,调用时直接 replace 替换变量。

Java

@Value("classpath:/prompts/user-identity-extraction-prompt.md")
Resource promptResource;
this.promptTemplate = promptResource.getContentAsString(UTF_8);
// 调用时
String prompt = promptTemplate
    .replace("{current_soul}", existingIdentity)
    .replace("{conversation_history}", conversationText);

为什么不用 PromptTemplate?

看一眼身份提取器的模板文件就明白了。

这个 .md 文件接近 200 行,前面定义角色(“你是一个专业的用户画像分析助手”),中间规定输出格式(Markdown 结构,包含求职偏好、教育背景、技能清单等 10 多个字段),后面给出 2 个完整的 Few-shot 示例教模型怎么做增量更新。

整个模板里只有 2 个变量占位符: {current_soul} 和 {conversation_history}。

这种“变量极少但模板极长”的场景,PromptTemplate 的模板引擎优势发挥不出来,反而是 .md 文件 + String.replace 最直接。

图片.png

模板放在 resources 目录下跟随代码一起进行版本管理,产品经理改一个输出字段不需要动 Java 代码,改完 .md 文件重启就生效。

求职派目前有 5 个以上的提示词模板文件分布在不同 Agent 模块的 resources/prompts/ 目录下,涵盖身份提取、灵魂画像、岗位信息抽取、会话摘要等场景。

图片.png

每个 Agent 的提示词独立管理,互不干扰。

提示词外置到资源文件,是 Spring AI 项目从 demo 走向生产几乎必经的一步。

04、LLM 调用封装

直接在业务代码里写 chatModel.call(prompt) 有问题吗?

有。

至少三个,每个 Agent 都要自己处理模型选择、上下文记忆管理、多模态消息构建。求职派为此抽象了一层调用器。

Java

public interface LlmCaller {
    String call(UserConversationInfo user, Prompt prompt);
    Flux stream(UserConversationInfo user, Prompt prompt);
}

三个实现类层层递进。

  • 轻量调用器,由调用者自己维护上下文,不带记忆也不注册工具。适合身份提取、会话摘要这类一次性调用,用完即走

  • 业务 Agent 调用器,加了 ChatMemory 做对话持久化,接入自定义的 ReAct 推理循环,工具调用经过中间件拦截。大部分业务 Agent(岗位抓取、身份收集)用这一层

  • 用户偏好调用器,在业务 Agent 基础上自动读取用户画像注入系统消息,同时注册文件系统、Shell、MCP、网页抓取等十余种工具。求职推荐 Agent 用的就是这个

每一层在上一层的基础上只增加必要的能力,业务代码按需选用,不必为简单场景付出复杂场景的开销。

图片.png

这里面有这样一个巧妙的设计,模型自动切换。当用户消息包含图片时,系统自动从文本模型切到视觉模型。

Java

if (prompt.getUserMessages().stream()
    .anyMatch(m -> !CollectionUtils.isEmpty(m.getMedia()))) {
    model = ModelConfig.ModelType.VISION;
}

业务 Agent 完全不用关心“用户发了图片该调哪个模型”,调用器在底层自动判断。

这种把基础设施决策下沉到公共层的做法,在多 Agent 系统里能显著降低每个 Agent 的实现复杂度。

05、历史消息怎么管理?

多轮对话场景下,Prompt 的消息列表会不断增长,最终超出大模型的上下文窗口。

求职派用智能窗口管理器解决了这个问题,分三步。

图片.png

第一步,过滤无用消息。正则匹配纯确认类回复,“好的”“嗯”“OK”“收到”这些不携带实际信息的消息直接过滤。

Java

Pattern.compile(
    "^(好的|收到|嗯|哦|好|知道了|明白了|ok|OK|yes)$",
    Pattern.CASE_INSENSITIVE);

只过滤用户消息,模型回复即使很短也保留,因为短回复可能是关键确认。

第二步,token 估算与裁剪。中文字符按每字 2 个 token 计算,英文单词按 1.3 个 token 计算,加上每条消息 10 个 token 的结构开销。

超出预算时从最旧的消息开始删除,但保留最近若干条不动。

第三步,总条数兜底。即使 token 没超,消息条数超过上限也会裁剪。

Spring AI 自带的 MessageWindowChatMemory 只支持按条数裁剪,不区分消息的信息密度。

一条“嗯”和一条 500 字的技术讨论在它眼里是等权的。

求职派在这个基础上做了两个改进,基于 token 的裁剪让预算控制更精确,无用消息过滤把有限的窗口留给真正有价值的内容。尤其在中文场景下,一个汉字约等于 2 个 token,如果不做区分估算,纯按英文假设计算会严重低估实际 token 消耗。

当对话更长时,求职派还有一层会话摘要机制作为补充。

超过一定轮数的历史消息会被 LLM 压缩成一段摘要,以一条新的消息注入回上下文,这样既不丢失早期对话的核心信息,又把 token 占用压到原来的十分之一左右。

窗口管理和会话摘要配合使用,保证了求职派在长对话场景下的稳定表现。

从提示词构建到上下文管理,整条线环环相扣——Prompt 承载消息和参数,Message 的角色决定信息的定位,模板让提示词可复用可维护,调用器在底层统一处理模型选择和身份注入,窗口管理器则确保历史消息不会撑爆上下文。

提示词是大模型交互的起点,也是最容易被低估的环节。

【把提示词当作工程问题来对待,大模型才会真正按我们想要的方式工作。】

06、如何把求职派写到简历上?

项目名称:求职派(JobClaw)多 Agent 求职系统

项目简介:基于 Spring AI 2.0 的多 Agent 求职平台,支持微信/钉钉/飞书多渠道接入,通过意图识别、Agent 路由、用户画像和上下文记忆,为用户提供身份采集、岗位抓取、岗位推荐和求职问答服务。

技术栈:Java 21、Spring Boot 4.0、Spring AI 2.0、LangGraph4J、Reactor、YAML、H2/MySQL

核心职责:

  • 基于 Spring AI 的 Prompt 和 ChatClient 抽象,封装 LLM 调用器(轻量/业务/用户偏好),统一处理模型选择、流式响应、会话记忆持久化和工具传参。

  • 将 9 个提示词模板外置为 Markdown 资源文件,覆盖身份提取、灵魂画像、岗位抽取、会话摘要等场景,提示词与 Java 代码解耦,支持多 Agent 独立迭代,产品侧改模板不需要动业务代码。

  • 实现智能上下文窗口管理,通过低效消息过滤、中英文 token 估算将多轮对话历史控制在预算内;支持超长对话触发异步摘要压缩,每 10 轮刷新一次,旧消息压缩为 100 字以内的摘要并归档。

  • 设计用户身份注入机制,按系统规则、性格画像、基础信息、求职偏好四层维护用户档案,自动从对话中增量提取并拼接到系统提示词,同一个推荐 Agent 面对不同用户给出差异化推荐结果。

  • 基于自定义 ReAct 替换 Spring AI 内置的自动工具执行,注册文件系统、Shell、MCP、网页抓取等十余种工具,并在推理-行动中插入中间件 Hook 钩子,支持 Function Calling 的日志记录和行为审计。

之前我们跑通了第一个 Spring AI 工程,大模型能正常回话了。

但把用户文本原封不动丢给模型,输出往往并不可靠。提示词是解决这些问题的关键钥匙,也是整个 Agent 系统的根基。

本篇内容第一版的作者:星球嘉宾灰灰。二哥做了二期的优化和迭代。

Spring AI 2.0 把提示词抽象成了 Prompt、Message、PromptTemplate 三层结构。包括身份注入处理、上下文管理、多模态消息、模型自动切换等。

图片.png

这篇内容将从基础概念出发,逐步串联到求职派的真实源码,让大家搞清楚提示词是如何从 API 概念到生产落地。

01、Prompt 不只是一段文本

很多教程把提示词等同于“一段文本”。

在大模型的 HTTP API 层面,请求体里确实有个 messages 数组。但 Spring AI 把它抽象成了一个更丰富的对象,包含消息列表和调用参数。

Java

public class Prompt implements ModelRequest> {
    private final List messages;
    private ChatOptions chatOptions;
}

messages 是一个消息列表,每条消息绑定一个角色。chatOptions 控制模型调用的行为参数,比如温度、模型名称。

图片.png

这个设计和 OpenAI API 的 messages 数组直接对应,Spring AI 用 Java 的 class 对象把 JSON 结构封装了一层。

创建 Prompt 最简单的方式是 new Prompt("给我讲个笑话"),Spring AI 自动把文本包装成一条用户消息。

但真实项目里几乎不会这么用,因为缺少系统指令,大模型不知道自己该扮演什么角色,输出质量全靠运气。如果想指定调用参数,就要传入第二个参数 ChatOptions。

Java

Prompt prompt = new Prompt(message,
    ZhiPuAiChatOptions.builder()
        .model("glm-5.2").temperature(0.7).build());

model 指定调用哪个模型,temperature 控制输出的随机性。

这两个参数几乎在每次调用时都会用到。搞清楚了 Prompt 的结构,下一个问题自然就来了,消息列表里的每条消息,角色是怎么分工的。

02、四种消息角色的分工

每条消息都有一个类型字段,和大模型定义的角色一一映射。

图片.png

  • system 负责系统指令,告诉大模型“该扮演什么角色、遵守什么规则”。

  • user 承载用户输入,文本和图片都绑定到 user

  • assistant 标记模型生成的内容,由框架自动管理

  • tool 对应函数调用的返回结果,Agent 的 ReAct 循环里会用到

四个角色里,我们直接操控的是 system 和 user。来看一个直观的例子,给大模型设定一个“儿童聊天助手”身份。

Java

Prompt prompt = new Prompt(
    List.of(new SystemMessage("你是专注于给3-5岁儿童聊天的助手"),
            new UserMessage(message)),
    ZhiPuAiChatOptions.builder()
        .model("glm-5.2").temperature(0.7).build());

加上 SystemMessage 后,同样问“讲个笑话”,输出明显更适合小朋友。

图片.png

temperature 设为 0.7 意味着要平衡确定性与多样性。

接近 0 时输出趋于保守,接近 1 时输出更有创造性但可能跑偏。实际业务场景里,问答类建议 0.3 左右,创意类可以调到 0.8。

求职派的业务 Agent 调用器在初始化时,会把用户画像和 Agent 指令拼接成一条系统消息。

Java

String identity = identityAgent.buildSoulPrompt(userId);
if (StringUtils.isNotBlank(identity)) {
    sys = buildSystemPrompt(identity + "\n\n" + systemPrompt);
}

这段逻辑先从身份系统拿到用户画像(教育背景、技能、求职偏好),再和 Agent 自身的系统指令拼在一起。

图片.png

同一个岗位的推荐 Agent,面对应届生和三年经验的开发者,会给出完全不同的推荐结果。系统消息不是写死的,而是跟着用户动态变化。

多模态场景下,用户消息还能携带图片和文件。

求职派从微信/钉钉/飞书收到的消息经过渠道适配后,文本和图片通过 UserMessage.builder() 统一装进一条用户消息(代码位置 core/.../BizAgentLlmCaller.java,第 203-209 行)。图片以 Media 对象的形式挂在 UserMessage 上,支持字节数组和文件路径两种来源。

Media 附件,决定了后面调文本模型还是视觉模型。

03、提示词模板

当提示词里有动态内容时,硬编码拼接会变得不可维护。Spring AI 提供了模板机制,用 {变量名} 做占位符。

Java

PromptTemplate tpl = new PromptTemplate(
    "你来扮演{personality}的{aiRole}, 我来扮演{myRole}");
Prompt prompt = tpl.create(Map.of(
    "personality", "温柔", "aiRole", "女朋友", "myRole", "男朋友"));

传入不同参数,大模型的表现截然不同。

图片.png

默认 create() 生成的是用户消息类型。

如果要生成系统提示词,换成 SystemPromptTemplate 调 createMessage() 即可。

在求职派里,PromptTemplate 并不是主力。系统提示词会 把 .md 文件当模板,用 String.replace 做变量替换。

以身份提取器为例,启动时通过 @Value 注入 classpath 下的 .md 文件,调用时直接 replace 替换变量。

Java

@Value("classpath:/prompts/user-identity-extraction-prompt.md")
Resource promptResource;
this.promptTemplate = promptResource.getContentAsString(UTF_8);
// 调用时
String prompt = promptTemplate
    .replace("{current_soul}", existingIdentity)
    .replace("{conversation_history}", conversationText);

为什么不用 PromptTemplate?

看一眼身份提取器的模板文件就明白了。

这个 .md 文件接近 200 行,前面定义角色(“你是一个专业的用户画像分析助手”),中间规定输出格式(Markdown 结构,包含求职偏好、教育背景、技能清单等 10 多个字段),后面给出 2 个完整的 Few-shot 示例教模型怎么做增量更新。

整个模板里只有 2 个变量占位符: {current_soul} 和 {conversation_history}。

这种“变量极少但模板极长”的场景,PromptTemplate 的模板引擎优势发挥不出来,反而是 .md 文件 + String.replace 最直接。

图片.png

模板放在 resources 目录下跟随代码一起进行版本管理,产品经理改一个输出字段不需要动 Java 代码,改完 .md 文件重启就生效。

求职派目前有 5 个以上的提示词模板文件分布在不同 Agent 模块的 resources/prompts/ 目录下,涵盖身份提取、灵魂画像、岗位信息抽取、会话摘要等场景。

图片.png

每个 Agent 的提示词独立管理,互不干扰。

提示词外置到资源文件,是 Spring AI 项目从 demo 走向生产几乎必经的一步。

04、LLM 调用封装

直接在业务代码里写 chatModel.call(prompt) 有问题吗?

有。

至少三个,每个 Agent 都要自己处理模型选择、上下文记忆管理、多模态消息构建。求职派为此抽象了一层调用器。

Java

public interface LlmCaller {
    String call(UserConversationInfo user, Prompt prompt);
    Flux stream(UserConversationInfo user, Prompt prompt);
}

三个实现类层层递进。

  • 轻量调用器,由调用者自己维护上下文,不带记忆也不注册工具。适合身份提取、会话摘要这类一次性调用,用完即走

  • 业务 Agent 调用器,加了 ChatMemory 做对话持久化,接入自定义的 ReAct 推理循环,工具调用经过中间件拦截。大部分业务 Agent(岗位抓取、身份收集)用这一层

  • 用户偏好调用器,在业务 Agent 基础上自动读取用户画像注入系统消息,同时注册文件系统、Shell、MCP、网页抓取等十余种工具。求职推荐 Agent 用的就是这个

每一层在上一层的基础上只增加必要的能力,业务代码按需选用,不必为简单场景付出复杂场景的开销。

图片.png

这里面有这样一个巧妙的设计,模型自动切换。当用户消息包含图片时,系统自动从文本模型切到视觉模型。

Java

if (prompt.getUserMessages().stream()
    .anyMatch(m -> !CollectionUtils.isEmpty(m.getMedia()))) {
    model = ModelConfig.ModelType.VISION;
}

业务 Agent 完全不用关心“用户发了图片该调哪个模型”,调用器在底层自动判断。

这种把基础设施决策下沉到公共层的做法,在多 Agent 系统里能显著降低每个 Agent 的实现复杂度。

05、历史消息怎么管理?

多轮对话场景下,Prompt 的消息列表会不断增长,最终超出大模型的上下文窗口。

求职派用智能窗口管理器解决了这个问题,分三步。

图片.png

第一步,过滤无用消息。正则匹配纯确认类回复,“好的”“嗯”“OK”“收到”这些不携带实际信息的消息直接过滤。

Java

Pattern.compile(
    "^(好的|收到|嗯|哦|好|知道了|明白了|ok|OK|yes)$",
    Pattern.CASE_INSENSITIVE);

只过滤用户消息,模型回复即使很短也保留,因为短回复可能是关键确认。

第二步,token 估算与裁剪。中文字符按每字 2 个 token 计算,英文单词按 1.3 个 token 计算,加上每条消息 10 个 token 的结构开销。

超出预算时从最旧的消息开始删除,但保留最近若干条不动。

第三步,总条数兜底。即使 token 没超,消息条数超过上限也会裁剪。

Spring AI 自带的 MessageWindowChatMemory 只支持按条数裁剪,不区分消息的信息密度。

一条“嗯”和一条 500 字的技术讨论在它眼里是等权的。

求职派在这个基础上做了两个改进,基于 token 的裁剪让预算控制更精确,无用消息过滤把有限的窗口留给真正有价值的内容。尤其在中文场景下,一个汉字约等于 2 个 token,如果不做区分估算,纯按英文假设计算会严重低估实际 token 消耗。

当对话更长时,求职派还有一层会话摘要机制作为补充。

超过一定轮数的历史消息会被 LLM 压缩成一段摘要,以一条新的消息注入回上下文,这样既不丢失早期对话的核心信息,又把 token 占用压到原来的十分之一左右。

窗口管理和会话摘要配合使用,保证了求职派在长对话场景下的稳定表现。

从提示词构建到上下文管理,整条线环环相扣——Prompt 承载消息和参数,Message 的角色决定信息的定位,模板让提示词可复用可维护,调用器在底层统一处理模型选择和身份注入,窗口管理器则确保历史消息不会撑爆上下文。

提示词是大模型交互的起点,也是最容易被低估的环节。

【把提示词当作工程问题来对待,大模型才会真正按我们想要的方式工作。】

06、如何把求职派写到简历上?

项目名称:求职派(JobClaw)多 Agent 求职系统

项目简介:基于 Spring AI 2.0 的多 Agent 求职平台,支持微信/钉钉/飞书多渠道接入,通过意图识别、Agent 路由、用户画像和上下文记忆,为用户提供身份采集、岗位抓取、岗位推荐和求职问答服务。

技术栈:Java 21、Spring Boot 4.0、Spring AI 2.0、LangGraph4J、Reactor、YAML、H2/MySQL

核心职责:

  • 基于 Spring AI 的 Prompt 和 ChatClient 抽象,封装 LLM 调用器(轻量/业务/用户偏好),统一处理模型选择、流式响应、会话记忆持久化和工具传参。

  • 将 9 个提示词模板外置为 Markdown 资源文件,覆盖身份提取、灵魂画像、岗位抽取、会话摘要等场景,提示词与 Java 代码解耦,支持多 Agent 独立迭代,产品侧改模板不需要动业务代码。

  • 实现智能上下文窗口管理,通过低效消息过滤、中英文 token 估算将多轮对话历史控制在预算内;支持超长对话触发异步摘要压缩,每 10 轮刷新一次,旧消息压缩为 100 字以内的摘要并归档。

  • 设计用户身份注入机制,按系统规则、性格画像、基础信息、求职偏好四层维护用户档案,自动从对话中增量提取并拼接到系统提示词,同一个推荐 Agent 面对不同用户给出差异化推荐结果。

  • 基于自定义 ReAct 替换 Spring AI 内置的自动工具执行,注册文件系统、Shell、MCP、网页抓取等十余种工具,并在推理-行动中插入中间件 Hook 钩子,支持 Function Calling 的日志记录和行为审计。