
做AI Agent的同学几乎都踩过记忆的坑。原型调试的时候对话少一切正常一上线用户聊个十几轮Agent突然就失忆了前面说过的业务背景全忘了用户得反复重复。更严重的token直接爆掉接口报错会话直接中断。很多人解决办法很粗暴把历史对话全塞给大模型。结果就是token成本直线飙升大模型输入超限被截断关键业务信息丢失甚至历史无关信息干扰当前回答出现张冠李戴的幻觉。生产环境的Agent记忆从来不是「把对话存起来」这么简单必须做分层设计。今天我们用Java完整实现三层记忆架构短期会话记忆、长期向量记忆、实体记忆代码可以直接复制进项目也能写进简历。本文属于JavaAI Agent落地系列上一篇我们讲了Function Calling生产级实现没看过可以翻历史文章如需配套资料只需关注图片水印即Java-AI工程师即打出agent可领取。一、三层记忆架构分别解决什么问题1. 短期记忆会话记忆保存当前会话的最近N轮对话只存活在本次会话里作用是维持上下文连贯性。核心是控制轮次上限避免token无限膨胀。一般保留8-12轮超出就淘汰最早的对话。2. 长期记忆向量记忆把重要的业务事实、用户偏好、历史决策向量化存入Milvus向量库跨会话持久化。新会话开启时根据用户当前问题召回相关历史信息不用把全部历史塞进prompt既省token又能做到「还记得你上次说过」。3. 实体记忆提取用户ID、租户、业务单据号这类关键参数单独结构化存储避免大模型幻觉篡改业务主键。避坑提醒向量记忆不是召回越多越好必须做相关性阈值过滤无关记忆会引入噪声反而让回答跑偏。二、Java完整代码实现1. 短期会话记忆轮次限制packagecom.aiproject.agent.memory;importjava.util.ArrayDeque;importjava.util.Deque;/** * 短期会话记忆固定轮次防止token爆炸 * 只保存当前会话最近N轮对话会话结束即销毁 */publicclassShortTermMemory{// 生产环境建议8-12轮根据模型窗口调整privatestaticfinalintMAX_ROUNDS10;privatefinalDequeMessagehistorynewArrayDeque();publicvoidaddUserMessage(Stringcontent){addMessage(user,content);}publicvoidaddAssistantMessage(Stringcontent){addMessage(assistant,content);}privatevoidaddMessage(Stringrole,Stringcontent){history.offerLast(newMessage(role,content));// 超出最大轮次淘汰最早的对话while(history.size()MAX_ROUNDS){history.pollFirst();}}/** * 组装成大模型可用的历史上下文 */publicStringbuildContext(){if(history.isEmpty()){return;}StringBuildersbnewStringBuilder(【历史对话】\n);for(Messagemsg:history){sb.append(msg.role()).append(: ).append(msg.content()).append(\n);}returnsb.toString();}/** * 获取当前历史轮次 */publicintsize(){returnhistory.size();}publicvoidclear(){history.clear();}publicrecordMessage(Stringrole,Stringcontent){}}2. 长期向量记忆Milvus完整实现packagecom.aiproject.agent.memory;importcom.alibaba.fastjson2.JSON;importcom.alibaba.fastjson2.JSONObject;importio.milvus.client.MilvusServiceClient;importio.milvus.grpc.DataType;importio.milvus.param.ConnectParam;importio.milvus.param.collection.CreateCollectionParam;importio.milvus.param.collection.FieldType;importio.milvus.param.dml.InsertParam;importio.milvus.param.dml.SearchParam;importio.milvus.response.SearchResultsWrapper;importokhttp3.*;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.stereotype.Component;importjavax.annotation.PostConstruct;importjava.io.IOException;importjava.util.ArrayList;importjava.util.Collections;importjava.util.List;importjava.util.concurrent.TimeUnit;/** * 长期向量记忆业务事实持久化跨会话召回 * 基于Milvus向量数据库 通义千问Embedding */ComponentpublicclassLongTermVectorMemory{Value(${milvus.host:localhost})privateStringmilvusHost;Value(${milvus.port:19530})privateintmilvusPort;Value(${llm.api-key})privateStringapiKey;Value(${llm.embedding-url:https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding})privateStringembeddingUrl;Value(${llm.embedding-model:text-embedding-v2})privateStringembeddingModel;// 向量维度text-embedding-v2是1536维privatestaticfinalintVECTOR_DIM1536;// 集合名privatestaticfinalStringCOLLECTION_NAMEagent_long_term_memory;// 相关性阈值低于此值的记忆不召回privatestaticfinalfloatSIMILARITY_THRESHOLD0.65f;privateMilvusServiceClientmilvusClient;privatefinalOkHttpClienthttpClientnewOkHttpClient.Builder().connectTimeout(10,TimeUnit.SECONDS).readTimeout(30,TimeUnit.SECONDS).build();PostConstructpublicvoidinit(){milvusClientnewMilvusServiceClient(ConnectParam.newBuilder().withHost(milvusHost).withPort(milvusPort).build());// 生产环境启动时确保集合存在ensureCollectionExists();}/** * 保存重要业务事实到向量库 */publicvoidsaveFact(StringtenantId,StringuserId,Stringfact){try{ListFloatvectorembed(fact);ListInsertParam.FieldfieldsnewArrayList();fields.add(newInsertParam.Field(tenant_id,Collections.singletonList(tenantId)));fields.add(newInsertParam.Field(user_id,Collections.singletonList(userId)));fields.add(newInsertParam.Field(content,Collections.singletonList(fact)));fields.add(newInsertParam.Field(vector,Collections.singletonList(vector)));milvusClient.insert(InsertParam.newBuilder().withCollectionName(COLLECTION_NAME).withFields(fields).build());}catch(Exceptione){thrownewRuntimeException(保存长期记忆失败,e);}}/** * 根据当前query召回TopN相关记忆带相关性过滤 */publicListStringrecall(StringtenantId,Stringquery,inttopN){try{ListFloatqueryVectorembed(query);SearchParamsearchParamSearchParam.newBuilder().withCollectionName(COLLECTION_NAME).withVectorFieldName(vector).withVectors(Collections.singletonList(queryVector)).withTopK(topN)// 租户隔离只召回当前租户的记忆.withExpr(tenant_id \tenantId\).build();SearchResultsWrapperwrappernewSearchResultsWrapper(milvusClient.search(searchParam).getData().getResults());ListStringresultsnewArrayList();List?rowRecordswrapper.getRowRecords(0);for(Objectrecord:rowRecords){SearchResultsWrapper.RowRecordrow(SearchResultsWrapper.RowRecord)record;floatscorerow.getScore();// 相关性阈值过滤低于阈值的记忆不召回避免噪声if(scoreSIMILARITY_THRESHOLD){Stringcontent(String)row.get(content);results.add(content);}}returnresults;}catch(Exceptione){thrownewRuntimeException(召回长期记忆失败,e);}}/** * 调用通义千问Embedding接口生成向量 */privateListFloatembed(Stringtext)throwsIOException{JSONObjectrequestBodynewJSONObject();requestBody.put(model,embeddingModel);requestBody.put(input,newString[]{text});RequestrequestnewRequest.Builder().url(embeddingUrl).addHeader(Authorization,Bearer apiKey).addHeader(Content-Type,application/json).post(RequestBody.create(requestBody.toJSONString(),MediaType.parse(application/json))).build();try(ResponseresponsehttpClient.newCall(request).execute()){Stringbodyresponse.body().string();JSONObjectjsonJSON.parseObject(body);returnjson.getJSONArray(output).getJSONObject(0).getJSONArray(embedding).toJavaList(Float.class);}}/** * 确保集合存在生产环境用运维脚本创建这里仅作演示 */privatevoidensureCollectionExists(){try{FieldTypetenantIdFieldFieldType.newBuilder().withName(tenant_id).withDataType(DataType.VarChar).withMaxLength(64).build();FieldTypeuserIdFieldFieldType.newBuilder().withName(user_id).withDataType(DataType.VarChar).withMaxLength(64).build();FieldTypecontentFieldFieldType.newBuilder().withName(content).withDataType(DataType.VarChar).withMaxLength(2000).build();FieldTypevectorFieldFieldType.newBuilder().withName(vector).withDataType(DataType.FloatVector).withDimension(VECTOR_DIM).build();milvusClient.createCollection(CreateCollectionParam.newBuilder().withCollectionName(COLLECTION_NAME).addFieldType(tenantIdField).addFieldType(userIdField).addFieldType(contentField).addFieldType(vectorField).build());}catch(Exceptionignored){// 集合已存在则忽略}}}3. 实体记忆关键参数结构化存储packagecom.aiproject.agent.memory;importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;/** * 实体记忆关键业务参数结构化存储防止大模型幻觉篡改 * 生产环境可替换为Redis或数据库持久化 */publicclassEntityMemory{// key: sessionId, value: 实体参数MapprivatefinalMapString,MapString,StringsessionEntitiesnewConcurrentHashMap();/** * 保存实体参数 */publicvoidputEntity(StringsessionId,Stringkey,Stringvalue){sessionEntities.computeIfAbsent(sessionId,k-newConcurrentHashMap()).put(key,value);}/** * 获取实体参数 */publicStringgetEntity(StringsessionId,Stringkey){MapString,StringentitiessessionEntities.get(sessionId);returnentitiesnull?null:entities.get(key);}/** * 组装实体信息注入prompt确保大模型使用正确的业务参数 */publicStringbuildEntityContext(StringsessionId){MapString,StringentitiessessionEntities.get(sessionId);if(entitiesnull||entities.isEmpty()){return;}StringBuildersbnewStringBuilder(【业务实体参数】\n);entities.forEach((k,v)-sb.append(k).append(: ).append(v).append(\n));sb.append(以上参数为系统确认值回答时必须使用不得自行编造或修改。\n);returnsb.toString();}publicvoidclear(StringsessionId){sessionEntities.remove(sessionId);}}三、生产踩过的3个坑不要全量召回向量记忆必须设置相似度阈值建议0.6-0.7低于阈值的记忆坚决不用否则噪声会引发幻觉。不要把用户输入全存长期记忆只存确认过的业务事实和用户偏好闲聊内容不要落库否则记忆库会越来越脏召回质量下降。短期记忆轮次不是越多越好超过12轮边际收益骤降token成本翻倍不如把关键信息沉淀到长期记忆。 落地资源推荐跑向量库和Agent后端推荐阿里云 轻量应用服务器2核4G跑MilvusSpringBoot完全够用新用户价格友好再加上通过推广渠道的9折优惠券跌破低价开发调试足够用有部署需求点击了解https://www.aliyun.com/daily-act/ecs/activity_selection?userCodefzoabro9阿里云开发者特惠无需求直接忽略。 领取完整源码关注图片上水印即Java-AI工程师 领取完整SpringBoot工程包含Milvus配置、Embedding工具、记忆过滤逻辑、单元测试。