AI工程师跑路了-怎么办?
从雪山飞狐到百年孤独
百无聊赖中翻开了又一本金庸的小说《雪山飞狐》,江湖侠气,快意恩仇瞬间跃然纸上,唯有最后胡斐那一刀才让读者回到了现实。之前刚读了《明朝那些事儿》,最后重墨了李闯王,也不知道是不是世界太小了,还是巧合,《雪山飞狐》的背景就是李闯王的4大护卫家的百年爱恨情仇,翻完《雪山飞狐》,又巧合的拿起了《百年孤独》,又是百年的孤独与爱恨... 这也许就是读书的另类乐趣吧。
Ai风吹进了公司
这两年好像出了这样一个论点:不搞ai的公司,好像都抬不起头。我们也不例外,来了一个老板,来了一个Ai工程师开始捣鼓起了Ai。对公司有这样的看法,自然视乎对我们这样的程序员也有同样的看法。于是我也一咬牙,一跺脚净身来到新的团队充数。想着边打杂跑腿,边抱大腿噌点Ai知识。之前都是弄几个demo自嗨,不得劲儿,想出来看看真的Ai项目长啥样。
哪成想,新来的Ai工程师用的Go语言,看不懂。于是吭哧吭哧学习了解了下Go,配合Cusor总算是马马虎虎看明白代码了。每个月140元包月的费用总算是没白瞎了。
大腿跑了我怎么办
本以为我在这个里从一个Javaer变一个Goer,可以在简历上写上精通GO,没成想,意外来得这么突然,Ai工程师跑了,大腿没有了,似乎短时间我要摇身一变成“大腿”了。但是留给我的时间不多,只有3周,其中还包括一个5.1长假。我陷入了深深的思索中,心中多个声音不断博弈:继续在原来的go上开发,能看和能写还是不是一会事儿,不可控;用Python实现Mult Agent,会demo到会写项目中间还间隔一个筋斗云,不可控;用java实现,自己是个熟练工,但没有一个成熟的框架可以用。
辗转反侧,夜不能寐,上下求索,各种尝试,进展微乎其微,觉得我的一只脚已经在公司外了。何以解忧,百年孤独。也许是巧合吧,当时的那种心情,与无人理解老何赛的科学追求,与无人理解到老乌尔苏拉如何维持大家庭的艰辛,与无人理解奥雷里亚诺上校战后的无奈,真可谓同病相连。
显然马尔克斯是懂人性的,是懂峰终定律的,所以几乎大部分布恩迪亚家的人,无论生前如何荒诞,生命的最后一刻都顿悟了,沐浴着最朴素的亲情。所以看是阴沉的书,给人的确实温暖。骆驼祥子,或者...整本书都是阴沉,阴沉,更阴沉,所有短暂的希望只是为了演绎更多失望。看完后只有一个感觉:绝望。
一边孤独,一边尝试,一边面试(除了招人,还希望能在面试过程中有所收获),终于看到了Spring Ai,欣喜若狂,突然感觉有了一根稻草。还没有等朝阳升起,一片乌云飘过 -- Srping Ai现在还只有里程碑版本 - M6。不幸中的万幸是 ,乌云不可怕,只要风够大 -- release版本会在5月发布。于是开始下定决心沿着javaer的路走下去。
有Sping Ai也不容易
Sping Ai要求JDK17,Spring boot 3.x 一个简单的要求差点让人崩溃。一开始想着用现有spring boot 2.3的项目直接升级到Spring boot 3.x,毕竟Spring boot 一直以向下兼容著称,但是架不住原来项目依赖不太规范,一顿操作后,无奈放弃。最后从朋友那里弄来一个Spring boot 3项目,总算是跑起来了,但是这只是刚刚开始。
因为不是relase版本, 不同版本 artifactId,包名,类名都都在变化,导致很多文章,甚至官方文档都是针对某个过去的特定版本编写的demo,这一切就如同面对马孔多南边一望无际的沼泽一般,老何赛终其一生也没能走出去,5.1长假鏖战数天,也没有完整跑起来一个dmeo。怎么都无法找到这个包。
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
秒针嘀嗒响个不停,时间如舟山的沙子从指间的飞快漏下,怎么都抓不住。漫长的煎熬下,甚至想再换个方向呢,换个思路呢,然而出门四顾心枉然,敢为路在何方!漫无目的的在Spring Ai 官方文档溜达(建议大家别看中文文档, 感觉是没有感情的机器翻译的),一个升级日志就默默的写那里,没有激动,没有喜悦,没有懊恼,靜靜换了Artifact ID ,demo自然的跑起来了,就像本该如何这般。
Artifact ID Changes
The naming pattern for Spring AI starter artifacts has changed. You’ll need to update your dependencies according to the following patterns:
Model starters:
spring-ai-{model}-spring-boot-starter
→spring-ai-starter-model-{model}
Vector Store starters:
spring-ai-{store}-store-spring-boot-starter
→spring-ai-starter-vector-store-{store}
MCP starters:
spring-ai-mcp-{type}-spring-boot-starter
→spring-ai-starter-mcp-{type}
Demo让时间流慢了
撑开拳头,沙子掉得慢了,时间仿佛也通了人性,尽可能的的速度缓慢流逝。虽然时间来到了5月7号,还有两周时间,但感觉有了更多的时间去丰富功能了。虽然不直接支持腾讯向量库(不建议使用腾讯向量库,特别是对图的处理,有点难受),但为自定义实现提供了良好的基础。如果恰好也有用这个向量库的,可以参考下。
/**
* @Author: JJ
* @CreateTime: 2025-05-07 14:21
* @Description: 腾讯向量存储
*/
@Slf4j
@Component
public class TencentVectorStore implements VectorStore { private final VectorDBClient client; public TencentVectorStore() { log.info("tencentVectorStore init");
log.info("tencentVectorStore init end");
} @Override
public void add(List<Document> documents) {
log.info("Adding " + documents.size() + " documents");
} @Override
public void delete(List<String> idList) {
log.info("Deleting " + idList.size() + " documents");
} @Override
public void delete(Filter.Expression filterExpression) {
log.info("Deleting filter " + filterExpression);
} @Override
public List<Document> similaritySearch(SearchRequest request) { AIDatabase db = client.aiDatabase("db");
CollectionView collection = db.describeCollectionView("cv1"); SearchOption searchOption = SearchOption.newBuilder()
.withChunkExpand(Arrays.asList(1,1))
.withRerank(new RerankOption(true, 2.5)) // 启用重排序,设置合理的召回倍率
.build();
SearchByContentsParam searchByContentsParam = SearchByContentsParam.newBuilder()
.withLimit(request.getTopK())
.withSearchContentOption(searchOption)
.withContent(request.getQuery())
.build();
log.info("searchByContentsParam {}", JSONUtil.toJsonStr(searchByContentsParam));
List<SearchContentInfo> searchRes = collection.search(searchByContentsParam); collection = db.describeCollectionView("cv2");
searchOption = SearchOption.newBuilder()
.withChunkExpand(Arrays.asList(1,1))
.withRerank(new RerankOption(true, 2.5)) // 启用重排序,设置合理的召回倍率
.build();
searchByContentsParam = SearchByContentsParam.newBuilder()
.withLimit(request.getTopK())
.withSearchContentOption(searchOption)
.withContent(request.getQuery())
.build();
log.info("searchByContentsParam-healthcare_with_img {}", JSONUtil.toJsonStr(searchByContentsParam));
List<SearchContentInfo> searchResWithImg = collection.search(searchByContentsParam);
searchRes.addAll(searchResWithImg); return searchRes.stream().filter((rowRecord) -> rowRecord.getScore() >= request.getSimilarityThreshold()).map((rowRecord) -> {
String docId = rowRecord.getDocumentSet().getDocumentSetId(); JsonObject metadata = new JsonObject(); StringBuilder knowledge = new StringBuilder();
rowRecord.getData().getPre().forEach(a->{
knowledge.append(a+"\n");
});
knowledge.append(rowRecord.getData().getText()+"\n");
rowRecord.getData().getNext().forEach(a->{
knowledge.append(a+"\n");
});
String content = knowledge.toString(); log.debug(JSONUtil.toJsonStr(rowRecord)); metadata = new JsonObject();
metadata.addProperty("documentSetName", rowRecord.getDocumentSet().getDocumentSetName());
metadata.addProperty(DocumentMetadata.DISTANCE.value(), 1.0F - rowRecord.getScore()); Gson gson = new Gson();
Type type = (new TypeToken<Map<String, Object>>() {
}).getType(); return Document.builder().id(docId).text(content).metadata(metadata != null ? (Map)gson.fromJson(metadata, type) : Map.of()).score(rowRecord.getScore()).build();
}).toList();
}
}
想自定义历史消息查询,也非常简单,毕竟默认的JDBC目前支持的数据库类型有限。只需要实现add 与 get 方法就可以了。 恰好有想自定义实现的同学也可以参靠下。
/**
* @Author: jijunjian
* @CreateTime: 2025-05-08 13:53
* @Description: 聊天记录
*/
@Slf4j
@Component
public class BellaChatMemory implements ChatMemory { @Resource
private ChatMessageV1Mapper chatMessageV1Mapper; @Resource
RedisService redisService; @Override
public void add(String conversationId, List<Message> messages) {
log.info("Saving " + messages.size() + " messages to conversation " + conversationId); MemberUserVo currentUser = MemberContextHolder.getCurrentUser(); String lastedMessageKey = RedisKey.lastedMessageKey(conversationId);
//用户的消息,手动添加,这里只添加系统的
messages.forEach(message -> { String userId = "0";
if (currentUser != null) {
userId = currentUser.getUserCode().toString();
} if (!message.getMessageType().equals(MessageType.USER)){
chatMessageV1Mapper.insert(chatMessageV1SaveReqVO);
}
}); } /**
* @param conversationId
* @param lastN
* @deprecated
*/
@Override
public List<Message> get(String conversationId) {
int lastN = 20;
log.debug("findByConversationId {}", conversationId);
LambdaQueryWrapper<ChatMessageV1DO> queryWrapperX = new LambdaQueryWrapperX<ChatMessageV1DO>()
.eq(ChatMessageV1DO::getConversationId, conversationId)
.eq(ChatMessageV1DO::getDeleted, 0)
.orderByDesc(ChatMessageV1DO::getId)
.last( "limit " + lastN); List<ChatMessageV1DO> chatMessageV1DOS = chatMessageV1Mapper.selectList(queryWrapperX);
//列表根据 id 升序
chatMessageV1DOS.sort((o1, o2) -> {
if (o1.getId() > o2.getId()) {
return 1;
} else if (o1.getId() < o2.getId()) {
return -1;
} else {
return 0;
}
});
List<Message> messageList = new ArrayList<>(); chatMessageV1DOS.forEach(messageDo -> {
var type = ChatRoleEnum.getByCode(messageDo.getAuthorRole());
switch (type) {
case USER:
String content = messageDo.getContent();
if(!Strings.isNullOrEmpty(messageDo.getImgs())){
content = content + "\n 用户图片:" + messageDo.getImgs();
}
messageList.add(new UserMessage(content));
break;
case ASSISTANT:
messageList.add(new AssistantMessage(messageDo.getContent()));
break;
default:
log.error("Unknown chat role " + messageDo.getAuthorRole());
};
});
return messageList;
} @Override
public void clear(String conversationId) {
log.debug("deleteByConversationId {}", conversationId);
}
}
两个特别留意的地方
Tool Calling 尝试100次都是无法调用,官方文档上的demo也无法运行,后来看到需要一个特别的参数配置才可以,于是101次成功了,但是102次又失败了,因为有些模型不支持Toos,于是103次成功了,也持续成功了。
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.internalToolExecutionEnabled(true)
ContextualQueryAugmenter 一定要重写 PromptTemplate 否则知识库中没有内容的话,总是回答不知道。还是直接在sping-ai原码中搜索才到这个提示,然后再针对性的解决了。原码中默认的PromptTemplate是这样的配置的。
private static final PromptTemplate DEFAULT_PROMPT_TEMPLATE = new PromptTemplate("""
Context information is below. ---------------------
{context}
--------------------- Given the context information and no prior knowledge, answer the query. Follow these rules: 1. If the answer is not in the context, just say that you don't know.
2. Avoid statements like "Based on the context..." or "The provided information...". Query: {query} Answer:
""");
于是这样初始化RetrievalAugmentationAdvisor问题就解决了。
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.queryTransformers(RewriteQueryTransformer.builder()
.chatClientBuilder(queryTransformerClient.mutate())
.build())
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(tencentVectorStore)
.build())
.queryAugmenter(ContextualQueryAugmenter.builder()
.allowEmptyContext(true)
.promptTemplate(contextualPrompt)
.build())
.build();
我也来践行峰终定律
有了上面的基础,522版本基本可控了,截至此时, MVP2.0算上基本上发布了,算是实现了阶段性目标, 那条悬在公司外的腿,又坚强的收了回来了,很明显,腿还小着,系统中定义了多个Agent去执行不同场景的任务,舌诊Agent完成图片的收集与实别,问卷Agent完成相关用户数据收集的场景... 再有意图识别route到具体worker Agent。实现了一个简单的chatBot不是啥大成果,但是有了迭代的基础,也因此有了希望。
后记
此版本语音模式体验不是太理想,一个字:慢。目前的方案是LLM输出后,才用miniMax转语音,这样是快不了的。也许接下来是时候去看看openai的 realtime 的框架了,因为目前只有Python SDK , 不知道有没有哪个朋友也给一个Python web 的项目。 想想就是一件高兴的事儿。
因为app还在 testflight 内测,我想想看看如何放到小程序让大家体验下。
官方不让放二维码,只能放一个链接了。
AI工程师跑路了-怎么办?的更多相关文章
- TM4C123G红外触摸屏:开发板好不容易实现了原理,放到专家设计的板子上无法运行,于是专家跑路项目黄了
使用TI的TM4C123G LaunchPad开发板,USB接口,来对同样的芯片进行烧写. 我们只用烧写那一块功能,不用另外一个芯片的开发功能,需要跳线 源码项目: 从官方网站TM4C123G ...
- 学会这个删库再也不用跑路了~ --技术流ken
前言 相信每一个学IT的人或多或少都听说过从删库到跑路这个梗~下图也是在各种交流群屡禁不止,新人听着也是瑟瑟发抖. 人们茶余饭后,街头巷角难免要问... 下面技术流ken就教给各位新手们一招删库再也不 ...
- 干货,不小心执行了rm -f,除了跑路,如何恢复?https://www.cnblogs.com/justmine/p/10359186.html
前言 每当我们在生产环境服务器上执行rm命令时,总是提心吊胆的,因为一不小心执行了误删,然后就要准备跑路了,毕竟人不是机器,更何况机器也有bug,呵呵. 那么如果真的删除了不该删除的文件,比如数据库. ...
- AI工程师职业规划和学习路线完整版
AI工程师职业规划和学习路线完整版 如何成为一名机器学习算法工程师 成为一名合格的开发工程师不是一件简单的事情,需要掌握从开发到调试到优化等一系列能 力,这些能力中的每一项掌握起来都需要足够的努力 ...
- 估值十亿美元、1.5亿用户,公司CEO却跑路了
转载这篇文章是觉得配图非常好玩的,文章的真实性有待证明 年收益3600万美元的.曾经拥有高口碑产品的Evernote,却正在把一手好牌打烂,距离IPO越来越远,屡屡被业界唱衰. "独角兽公司 ...
- Linux设备驱动工程师之路——内核链表的使用【转】
本文转载自:http://blog.csdn.net/forever_key/article/details/6798685 Linux设备驱动工程师之路——内核链表的使用 K-Style 转载请注明 ...
- 洛谷P1613 跑路
P1613 跑路 176通过 539提交 题目提供者该用户不存在 标签倍增动态规划 难度普及+/提高 提交该题 讨论 题解 记录 最新讨论 这个题的数据.. 题意问题 表意 题目描述 小A的工作不仅繁 ...
- 腾讯云总监手把手教你,如何成为AI工程师?
作者:朱建平 腾讯云技术总监,腾讯TEG架构平台部专家工程师 1.关于人工智能的若干个错误认知 人工智能是AI工程师的事情,跟我没有什么关系 大数据和机器学习(AI) 是解决问题的一种途径和手段,具有 ...
- 编程从入门到提高,然后放弃再跑路(Java)
1.Java入门篇 1.1 基础入门和面向对象 1.1.1 编程基础 [01] Java语言的基本认识 [02] 类和对象 [03] 类的结构和创建对象 [04] 包和访问权限修饰符 [05] 利用p ...
- Luogu1613 跑路
题目描述 小A的工作不仅繁琐,更有苛刻的规定,要求小A每天早上在6:00之前到达公司,否则这个月工资清零.可是小A偏偏又有赖床的坏毛病.于是为了保住自己的工资,小A买了一个十分牛B的空间跑路器,每秒钟 ...
随机推荐
- 大数据之路Week10_day05 (Redis总结I)
正文 1.为什么使用redis 分析:博主觉得在项目中使用redis,主要是从两个角度去考虑:性能和并发.当然,redis还具备可以做分布式锁等其他功能,但是如果只是为了分布式锁这些其他功能,完全还有 ...
- Vue3组合式API终极指南:从原理到实战,彻底掌握高效开发!
前言 在Vue3从发布到今天,组合式API已成为现代前端开发的标杆设计模式.本文通过真实项目场景,深度解析组合式API的核心特性,配以完整代码示例,助你彻底掌握企业级Vue应用开发精髓. 一.为什么组 ...
- Web前端入门第 6 问:HTML 的基础语法结构
HTML的全称为超文本标记语言(HyperText Markup Language),基础语法结构由标签.元素.属性和内容组成,遵循层级嵌套的树形结构. 关键语法规则 标签(Tags) 双标签语法 标 ...
- 【FAQ】HarmonyOS SDK 闭源开放能力 —Push Kit(10)
1.问题描述: 离线推送,锁屏的时候没有弹出消息,只有下拉在通知中心里面显示.请问是否是正常的? 解决方案: 检查一下是否存在图片风控:https://developer.huawei.com/con ...
- 文件批量重命名神器:Bulk Rename Utility
内容摘要: 你还在手动给文件重命名吗?介绍一款免费而强大的批量重命名神器:Bulk Rename Utility,它将满足你对批量改名的所有期待.让它将你从痛苦的重命名工作中解放吧! 软件获取地址 云 ...
- ORA-01779: 无法修改与非键值保存表对应的列”中涉及的概念和解决方法
什么是键值保存表(Key-Preserved Table)? 在理解什么是键值保存表之前,首先要知道 可更新的联接视图 这个概念,键值保存表只是保存了允许更新的字段信息的一张表.为什么会出现这么一张表 ...
- Docker Swarm Mode 的容器资源回收问题
问题描述 Docker Swarm Mode 中 service 的update/scale等操作都会形成残留的容器和镜像,会造成一定程度的磁盘空间占用及缓存占用等问题... 解析 存在即合理,残留的 ...
- HTML5
转
贴个图:
- FireDAC开发DataSnap应用系统【3】-使用TFDJSONDatasets的CRUD功能
类别 说明 TFDJSONDeltas 包含异动的delta的类别.客户端存放deltade对象 TFDJSONDeltasWriter 把deltas写入TFDJSONDeltas TFDJSOND ...
- .NET周刊【4月第1期 2025-04-06】
国内文章 35岁程序员的中年求职记:四次碰壁后的深度反思 https://www.cnblogs.com/minily/p/18803259 文章探讨程序员的35岁危机,指出这一问题确实存在,但也有其 ...