<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      AI工程師跑路了-SpringAi來幫忙

      ?

       從雪山飛狐到百年孤獨

      百無聊賴中翻開了又一本金庸的小說《雪山飛狐》,江湖俠氣,快意恩仇瞬間躍然紙上,唯有最后胡斐那一刀才讓讀者回到了現實。之前剛讀了《明朝那些事兒》,最后重墨了李闖王,也不知道是不是世界太小了,還是巧合,《雪山飛狐》的背景就是李闖王的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 內測,我想想看看如何放到小程序讓大家體驗下。
      官方不讓放二維碼,只能放一個鏈接了

       

       


      ?
      posted @ 2025-05-26 09:56  2J  閱讀(1229)  評論(15)    收藏  舉報
      主站蜘蛛池模板: 国模一区二区三区私拍视频| 无码午夜福利片| 精品人妻av区乱码| 一区二区不卡99精品日韩| 内射一区二区三区四区| 中文字幕人妻熟女人妻a片| 97超级碰碰碰久久久久| 国产精品美女久久久久久麻豆| 欧美国产日韩久久mv| 国产精品国产三级国产午| 福利视频一区二区在线| 苍山县| 乱人伦人妻精品一区二区| 老司机午夜精品视频资源| 民权县| 国产怡春院无码一区二区| 亚洲欧洲日产国码高潮αv| 2019久久久高清日本道| 亚洲av天堂天天天堂色| 99久久免费精品色老| 成人无码视频| 国产精品国三级国产av| 广元市| 中文字幕乱码中文乱码毛片| 亚洲真人无码永久在线| 国产精品午夜福利在线观看 | 丁香五月婷激情综合第九色| 亚洲成人精品在线伊人网| 国产免费无遮挡吸奶头视频| 久久人妻精品大屁股一区| 岛国最新亚洲伦理成人| 免费超爽大片黄| 欧美奶涨边摸边做爰视频| 国产成人精品无码免费看| 蜜芽久久人人超碰爱香蕉| 无码人妻一区二区三区AV| 性无码专区无码| 免费的特黄特色大片| 亚洲日韩VA无码中文字幕| 久久热精品视频在线视频| 国产在线不卡精品网站|