LangChain4j之AiService结构化输出踩坑记录
·
环境
jdk 17
springboot 3.0.2
maven 3.6.3
需求
需要让大模型根据输入的姓名首字母和目标长度生成推荐的姓名和对应的寓意,要求返回如下的JSON数组给前端:
[
{
"name": "吴海",
"meaning": "寓意胸怀宽广,如大海般包容,志向远大。"
},
{
"name": "吴阳",
"meaning": "寓意光明磊落,前程似锦,充满希望。"
},
......
]
踩坑记录
已经配置好了大模型和AiService:
@Configuration
public class LLMConfig {
@Bean(name = "qwen")
public ChatModel QwenChatModel() {
return OpenAiChatModel.builder()
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.apiKey(System.getenv("qwen-api-key"))
.modelName("qwen-plus")
.logRequests(true)
.logResponses(true)
.build();
}
@Bean
public ChatAssistant chatAssistant(@Qualifier("qwen") ChatModel chatModel) {
return AiServices.builder(ChatAssistant.class)
.chatModel(chatModel)
.inputGuardrails()
.build();
}
}
AiSercice对应的接口:
public interface ChatAssistant {
String chat(String prompt);
@SystemMessage("你是一个专业的起名专家,需要根据用户指定的姓名长度和姓名的第一个字生成10个中文名字,并给出每个名字的寓意")
List<NameOutput> generateNames(String prompt);
}
调用方法时报错:
java.lang.IllegalStateException: null
at dev.langchain4j.service.output.PojoCollectionOutputParser.formatInstructions(PojoCollectionOutputParser.java:57) ~[langchain4j-1.8.0.jar:na]
at dev.langchain4j.service.output.ServiceOutputParser.outputFormatInstructions(ServiceOutputParser.java:108) ~[langchain4j-1.8.0.jar:na]
at dev.langchain4j.service.DefaultAiServices$1.appendOutputFormatInstructions(DefaultAiServices.java:502) ~[langchain4j-1.8.0.jar:na]
at dev.langchain4j.service.DefaultAiServices$1.invoke(DefaultAiServices.java:301) ~[langchain4j-1.8.0.jar:na]
at dev.langchain4j.service.DefaultAiServices$1.invoke(DefaultAiServices.java:236) ~[langchain4j-1.8.0.jar:na]
at jdk.proxy2/jdk.proxy2.$Proxy53.generateNames(Unknown Source) ~[na:na]
at com.example.langchain4j.controller.ChatController.generateNames(ChatController.java:27) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884) ~[spring-webmvc-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[spring-webmvc-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1080) ~[spring-webmvc-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:973) ~[spring-webmvc-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011) ~[spring-webmvc-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[spring-webmvc-6.0.4.jar:6.0.4]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:731) ~[tomcat-embed-core-10.1.5.jar:6.0]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.0.4.jar:6.0.4]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:814) ~[tomcat-embed-core-10.1.5.jar:6.0]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:223) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-10.1.5.jar:10.1.5]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.4.jar:6.0.4]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.4.jar:6.0.4]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.4.jar:6.0.4]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:177) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:400) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:859) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1734) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na]
解决方法
参考GitHub上的issue:Structured outputs: Support List with prompting
1、如果需要结构化输出,配置ChatModel的时候需要配置JSON响应格式支持并严格输出JSON响应格式:
@Bean(name = "qwen")
public ChatModel QwenChatModel() {
return OpenAiChatModel.builder()
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.apiKey(System.getenv("qwen-api-key"))
.modelName("qwen-plus")
.logRequests(true)
.logResponses(true)
.supportedCapabilities(Capability.RESPONSE_FORMAT_JSON_SCHEMA) // 添加 JSON 响应格式支持
.strictJsonSchema(true) // 严格输出 JSON 响应格式
.build();
}
2、需要在输出映射类中添加无参构造方法,springboot中可以通过@NoArgsConstructor注解实现
@Data
@AllArgsConstructor
@NoArgsConstructor
public class NameOutput {
@Description("名称")
private String name;
@Description("寓意")
private String meaning;
}
注意:不是所有大模型都支持以配置的方式规定格式化输出,目前测试下来Qwen、gemini、gork是没问题的,对于那些不支持的大模型,就只能在SystemMessage里对其输出格式进行提示,但这种方式不能保证稳定性
效果
整体的项目结构:
控制层:
@Slf4j
@RestController
@RequestMapping("/chat")
public class ChatController {
@Autowired
private ChatAssistant chatAssistant;
@PostMapping(value = "/generateNames", produces = "application/json;charset=utf-8")
public List<NameOutput> generateNames(@RequestBody NameRequest request) {
String prompt = String.format("请根据以下条件生成中文名字:第一个字是'%s',总长度为%d个字", request.getFirstChar(), request.getLength());
return chatAssistant.generateNames(prompt);
}
}
最后的效果:
前端页面代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI取名器</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;700&family=Noto+Sans+SC:wght@300;400;500&display=swap"
rel="stylesheet">
<style>
/* 全局变量定义 */
:root {
--primary-color: #4a6fa5; /* 靛青 */
--primary-hover: #34517b;
--bg-gradient: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
--card-bg: rgba(255, 255, 255, 0.95);
--text-main: #2c3e50;
--text-sub: #7f8c8d;
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.08);
--radius-main: 16px;
}
body {
font-family: 'Noto Sans SC', sans-serif;
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-gradient);
background-attachment: fixed;
color: var(--text-main);
padding: 20px;
box-sizing: border-box; /* 确保padding不撑大宽度 */
}
.container {
background-color: var(--card-bg);
padding: 40px;
border-radius: var(--radius-main);
box-shadow: var(--shadow-soft);
width: 100%;
/* 修改:增加最大宽度以适应左右布局 */
max-width: 900px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.5);
transition: transform 0.3s ease;
/* 确保容器内元素溢出处理 */
overflow: hidden;
}
/* 新增:左右布局包装器 */
.layout-wrapper {
display: flex;
gap: 40px; /* 左右间距 */
align-items: flex-start;
}
/* 新增:左侧面板(输入区) */
.left-panel {
flex: 0 0 350px; /* 固定宽度,看起来更整洁 */
display: flex;
flex-direction: column;
}
/* 新增:右侧面板(结果区) */
.right-panel {
flex: 1; /* 占据剩余空间 */
min-height: 400px; /* 给右侧一个最小高度,保持平衡 */
background: rgba(245, 247, 250, 0.5); /* 轻微背景区分 */
border-radius: 12px;
padding: 20px;
border: 1px dashed #e0e0e0;
/* 内部滚动 */
max-height: 550px;
overflow-y: auto;
position: relative;
}
/* 美化右侧滚动条 */
.right-panel::-webkit-scrollbar {
width: 6px;
}
.right-panel::-webkit-scrollbar-thumb {
background-color: #cbd5e0;
border-radius: 3px;
}
.right-panel::-webkit-scrollbar-track {
background: transparent;
}
h1 {
font-family: 'Noto Serif SC', serif;
text-align: center;
color: var(--text-main);
margin-bottom: 30px;
margin-top: 0; /* 修正顶部间距 */
font-size: 2rem;
letter-spacing: 2px;
position: relative;
}
h1::after {
content: '';
display: block;
width: 60px;
height: 3px;
background: var(--primary-color);
margin: 10px auto 0;
border-radius: 2px;
opacity: 0.7;
}
.form-group {
margin-bottom: 25px;
position: relative;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
font-size: 0.95rem;
color: var(--text-main);
}
input[type="text"], input[type="number"] {
width: 100%;
padding: 14px 16px;
border: 2px solid #e0e0e0;
border-radius: 12px;
font-size: 16px;
box-sizing: border-box;
transition: all 0.3s ease;
background-color: #f9f9f9;
font-family: inherit;
}
input:focus {
border-color: var(--primary-color);
background-color: #fff;
outline: none;
box-shadow: 0 0 0 4px rgba(74, 111, 165, 0.1);
}
button {
background: linear-gradient(to right, #4a6fa5, #6b8cce);
color: white;
padding: 15px;
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
width: 100%;
letter-spacing: 1px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(74, 111, 165, 0.3);
margin-top: 10px;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(74, 111, 165, 0.4);
}
button:active {
transform: translateY(0);
}
button:disabled {
background: #cbd5e0;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 结果区域样式优化 */
.result {
/* margin-top: 30px; 移除顶部间距,由padding控制 */
animation: fadeIn 0.5s ease-in-out;
width: 100%;
}
.result h2 {
font-size: 1.2rem;
color: var(--text-sub);
border-left: 4px solid var(--primary-color);
padding-left: 10px;
margin-bottom: 20px;
margin-top: 0; /* 修正结果标题间距 */
font-weight: normal;
position: sticky;
top: 0;
background: transparent;
}
.name-item {
background: white;
margin-bottom: 15px;
padding: 20px;
border-radius: 12px;
border: 1px solid #eee;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.name-item:hover {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
border-color: var(--primary-color);
}
.name {
font-family: 'Noto Serif SC', serif;
font-size: 24px;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 8px;
}
.meaning {
font-size: 14px;
color: var(--text-sub);
line-height: 1.6;
text-align: justify;
}
.error {
background-color: #fff5f5;
color: #c53030;
padding: 15px;
border-radius: 12px;
border: 1px solid #feb2b2;
margin-top: 20px;
display: flex;
align-items: center;
font-size: 0.95rem;
}
/* 空状态提示 */
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #bdc3c7;
font-size: 0.9rem;
text-align: center;
min-height: 360px;
}
.loading {
text-align: center;
color: var(--primary-color);
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
height: 100%; /* 居中显示 */
}
.loading::before {
content: '';
width: 18px;
height: 18px;
border: 3px solid var(--primary-color);
border-top-color: transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式适配:平板和手机恢复上下布局 */
@media (max-width: 768px) {
.container {
padding: 25px;
max-width: 500px; /* 恢复原来的窄宽度 */
}
.layout-wrapper {
flex-direction: column; /* 改为垂直排列 */
gap: 30px;
}
.left-panel {
flex: none;
width: 100%;
}
.right-panel {
width: 100%;
min-height: auto; /* 取消最小高度限制 */
max-height: none; /* 取消滚动限制 */
overflow: visible;
box-sizing: border-box;
}
h1 {
font-size: 1.6rem;
}
}
</style>
</head>
<body>
<div class="container">
<!-- 新增 layout-wrapper 用于左右布局控制 -->
<div class="layout-wrapper">
<!-- 左侧面板:标题和表单 -->
<div class="left-panel">
<h1>AI取名器</h1>
<form id="nameForm">
<div class="form-group">
<label for="firstChar">姓名首字</label>
<input type="text" id="firstChar" maxlength="1" required placeholder="请输入姓名的第一个字,例如:李">
</div>
<div class="form-group">
<label for="length">生成长度</label>
<input type="number" id="length" min="1" max="10" value="3" required placeholder="建议输入2-3">
</div>
<button type="submit" id="submitBtn">开始生成</button>
</form>
</div>
<!-- 右侧面板:结果展示区 -->
<div class="right-panel">
<div id="result">
<!-- 初始空状态占位符 -->
<div class="empty-state">
等待生成...<br>结果将显示在这里
</div>
</div>
</div>
</div>
</div>
<script>
// 原始逻辑保持完全不变
document.getElementById('nameForm').addEventListener('submit', function (e) {
e.preventDefault();
const firstChar = document.getElementById('firstChar').value.trim();
const length = parseInt(document.getElementById('length').value);
const resultDiv = document.getElementById('result');
const submitBtn = document.getElementById('submitBtn');
// 验证输入
if (!firstChar) {
showError('请输入姓名首字');
return;
}
if (isNaN(length) || length < 1 || length > 10) {
showError('姓名长度必须在1到10之间');
return;
}
// 显示加载状态
submitBtn.disabled = true;
submitBtn.innerHTML = '正在推演中...';
showLoading();
// 准备请求数据
const requestData = {
firstChar: firstChar,
length: length
};
// 发送请求
fetch('/chat/generateNames', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(requestData)
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应错误: ' + response.status);
}
return response.json();
})
.then(data => {
displayResults(data);
})
.catch(error => {
console.error('Error:', error);
showError('生成姓名时出错: ' + error.message);
})
.finally(() => {
// 恢复按钮状态
submitBtn.disabled = false;
submitBtn.textContent = '生成姓名';
});
});
function showLoading() {
document.getElementById('result').innerHTML = '<div class="loading">正在为您推演好名...</div>';
}
function displayResults(names) {
const resultDiv = document.getElementById('result');
if (!names || names.length === 0) {
resultDiv.innerHTML = '<div class="result" style="text-align:center; color:#666; padding-top:20px;">未生成任何姓名</div>';
return;
}
let html = '<div class="result"><h2>生成结果</h2>';
names.forEach(nameObj => {
html += `
<div class="name-item">
<div class="name">${nameObj.name}</div>
<div class="meaning">${nameObj.meaning}</div>
</div>
`;
});
html += '</div>';
resultDiv.innerHTML = html;
}
function showError(message) {
document.getElementById('result').innerHTML = `<div class="error">⚠️ ${message}</div>`;
}
</script>
</body>
</html>
maven依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- open-ai -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>1.8.0</version>
</dependency>
<!-- AiService -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.8.0</version>
</dependency>
</dependencies>
更多推荐



所有评论(0)