Visual Studio 修改NuGet 包缓存路径 - Earen - 博客园

mikel阅读(427)

来源: Visual Studio 修改NuGet 包缓存路径 – Earen – 博客园

Visual Studio 下载的NuGet包默认会缓存到 C:\Users{Windows用户名}.nuget\packages 下,时间一长就会导致 C盘空间严重不足。

那么怎样去设置,让包缓存文件保存到其他盘呢?

首先我们要找到 Microsoft.VisualStudio.Offline.config 这个文件,它在哪呢? 在 C:\Program Files (x86)\NuGet\Config。

再到文件并用记事本打开 如下:

我们在中间增加如下配置内容:

  <config>
      <add key="globalPackagesFolder" value="D:\Nuget\.nuget\packages" />
  </config>

最终效果如下图:

这样就配置好了,重新打开 Visual Studio 下载 NuGet 包文件后,查看包的引用地址就会发现地址为修改后的地址。

温馨提示:在配置好地址后,可将原来已经缓存的包文件全部拷贝到新的目录中。

Spring AI与DeepSeek实战四:系统API调用 - zlt2000 - 博客园

mikel阅读(393)

来源: Spring AI与DeepSeek实战四:系统API调用 – zlt2000 – 博客园

一、概述

在 AI 应用开发中,工具调用 Tool Calling 是增强大模型能力的核心技术。通过让模型与外部 API 或工具交互,可实现 实时信息检索(如天气查询、新闻获取)、系统操作(如创建任务、发送邮件)等功能。

Spring AI 作为企业级 AI 开发框架,在 1.0.0.M6 版本中进行了重要升级:废弃 Function Calling 引入 Tool Calling 以更贴合行业术语;本文结合 Spring AI 与大模型,演示如何通过 Tool Calling 实现系统 API 调用,同时处理多轮对话中的会话记忆。

关于 Spring AI 与 DeepSeek 的集成,以及 API-KEY 的申请等内容,可参考文章《Spring AI与DeepSeek实战一:快速打造智能对话应用

 

二、函数调用原理

大模型仅负责 决定是否调用工具 和 提供参数,实际执行逻辑由客户端(Spring 应用)实现,确保工具调用的可控性与安全性。

2.1. 工具元数据注入

在发起Chat Request时,将工具描述(name/description)、参数结构(input schema)等元数据封装至请求体,建立大模型的工具调用能力基线。

2.2. 模型决策响应

大模型根据上下文推理生成工具调用指令(tool_calls字段),返回包含选定工具名称及结构化参数的中间响应。

2.3. 服务端路由执行

Spring AI模块解析工具调用指令,通过服务发现机制定位目标工具实例,注入参数并触发同步/异步执行。

2.4. 执行结果标准化

工具返回原始执行结果后,系统进行数据类型校验、异常捕获和JSON序列化处理,生成模型可解析的标准化数据结构。

2.5. 上下文增强推理

将标准化结果作为新增上下文(tool_outputs)回传大模型,触发基于增强上下文的二次推理流程。

2.6. 终端响应生成

模型综合初始请求与工具执行结果,生成最终自然语言响应,完成工具增强的对话闭环。

 

三、核心代码

3.1. 定义工具

创建类 TestTools 并用 @Tool 注解定义 tool 的描述

public static class TestTools {
    @Tool(description = "获取今天日期")
    String getCurrentDateTime() {
        System.out.println("======getCurrentDateTime");
        return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"));
    }

    @Tool(description = "获取当前温度")
    String getCurrentTemperature(MyToolReques  toolReques) {
        System.out.println("======getCurrentTemperature: " + toolReques.localName + "__" + toolReques.date);
        return toolReques.date + toolReques.localName + "温度为20摄氏度";
    }

    public record MyToolReques(String localName, String date) {}
}

这里定义了两个方法,并通过注解的 description 参数告诉大模型方法的用途。

record 类型是 Java17 的新特性,可用于替代传统的 POJO 类。

3.2. 创建对话接口

private ChatMemory chatMemory = new InMemoryChatMemory();
private MessageChatMemoryAdvisor messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory);

@GetMapping(value = "/chat")
public String chat(@RequestParam String input, String sessionId, HttpServletResponse response) {
    response.setCharacterEncoding("UTF-8");

    return chatClient.prompt().user(input)
            .tools(new TestTools())
            .advisors(messageChatMemoryAdvisor)
            .advisors(spec -> spec
                    .param(MessageChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId))
            .call()
            .content();
}
  • tools 给大模型注册可以调用的方法。
  • MessageChatMemoryAdvisor 实现聊天记忆,InMemoryChatMemory 为 SpringAI 自带的实现(基于内存)。
  • 可以使用 CHAT_MEMORY_CONVERSATION_ID_KEY 参数指定对话ID,不同的会话ID用于隔离记忆。

3.3. 测试

通过后台打印信息可以看到大模型会自动识别,先调用 getCurrentDate 方法获取今天日期,再调用 getCurrentTemperature 方法获取天气。

======getCurrentDate

======getCurrentTemperature: 广州__2025年04月13日

 

加一个聊天框,测试多轮对话效果:

第二次会话直接输入 广州 大模型就知道我要问什么了,说明聊天记忆功能已生效。

 

四、总结

本文以问天气为场景,通过 Tool Calling 实现系统 API 调用,同时实现多轮对话中的会话记忆。需要注意的是 InMemoryChatMemory 只能作为测试使用,在企业应用中需要使用其他实现方式,把聊天记录存储在 Redis 或者 数据库中,并且需要考虑消息的保存时间、容量、如何清除等问题。

 

五、完整代码

  • Gitee地址:

https://gitee.com/zlt2000/zlt-spring-ai-app

  • Github地址:

https://github.com/zlt2000/zlt-spring-ai-app

Spring AI与DeepSeek实战三:打造企业知识库 - zlt2000 - 博客园

mikel阅读(1264)

来源: Spring AI与DeepSeek实战三:打造企业知识库 – zlt2000 – 博客园

一、概述

企业应用集成大语言模型(LLM)落地的两大痛点:

  • 知识局限性:LLM依赖静态训练数据,无法覆盖实时更新或垂直领域的知识;
  • 幻觉:当LLM遇到训练数据外的提问时,可能生成看似合理但错误的内容。

用最低的成本解决以上问题,需要使用 RAG 技术,它是一种结合信息检索技术与 LLM 的框架,通过从外部 知识库 动态检索相关上下文信息,并将其作为 Prompt 融入生成过程,从而提升模型回答的准确性;

本文将以AI智能搜索为场景,基于 Spring AI 与 RAG 技术结合,通过构建实时知识库增强大语言模型能力,实现企业级智能搜索场景与个性化推荐,攻克 LLM 知识滞后与生成幻觉两大核心痛点。

关于 Spring AI 与 DeepSeek 的集成,以及 API-KEY 的申请等内容,可参考文章《Spring AI与DeepSeek实战一:快速打造智能对话应用

 

二、RAG数据库选择

构建知识库的数据库一般有以下有两种选择:

维度 向量数据库 知识图谱
数据结构 非结构化数据(文本/图像向量) 结构化关系网络(实体-关系-实体)
查询类型 语义相似度检索 多跳关系推理
典型场景 文档模糊匹配、图像检索 供应链追溯、金融风控
性能指标 QPS>5000 复杂查询响应时间>2s
开发成本 低(API即用) 高(需构建本体模型)

搜索推荐场景更适合选择 向量数据库

 

三、向量模型

向量模型是实现 RAG 的核心组件之一,用于将非结构化数据(如文本、图像、音频)转换为 高维向量(Embedding)的机器学习模型。这些向量能够捕捉数据的语义或结构信息,使计算机能通过数学运算处理复杂关系。

向量数据库是专门存储、索引和检索高维向量的数据库系统

spring-ai-alibaba-starter 默认的向量模型为 text-embedding-v1

可以通过 spring.ai.dashscope.embedding.options.model 进行修改。

 

四、核心代码

4.1. 构建向量数据

创建 resources/rag/data-resources.txt 文件,内容如下:

1. {"type":"api","name":"测试api服务01","topic":"综合政务","industry":"采矿业","remark":"获取采矿明细的API服务"}
2. {"type":"api","name":"新能源车类型","topic":"能源","industry":"制造业","remark":"获取新能源车类型的服务"}
3. {"type":"api","name":"罚款报告","topic":"交通","industry":"制造业","remark":"获取罚款报告的接口"}
4. {"type":"api","name":"光伏发电","topic":"能源","industry":"电力、热力、燃气及水生产和供应业","remark":"获取光伏发电的年度报告"}
5. {"type":"api","name":"收益明细2025","topic":"综合政务","industry":"信息传输、软件和信息技术服务业","remark":"2025年的收益明细信息表"}

创建向量数据库的 Bean

@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel
        , @Value("classpath:rag/data-resources.txt") Resource docs) {
    VectorStore vectorStore = SimpleVectorStore.builder(embeddingModel).build();
    vectorStore.write(new TokenTextSplitter().transform(new TextReader(docs).read()));
    return vectorStore;
}
  • SimpleVectorStore 是 Spring AI 提供的一个基于内存的向量数据库;
  • 使用 TokenTextSplitter 来切分文档。

4.2. 创建ChatClient

private final ChatClient chatClient;

public RagController(ChatClient.Builder builder, VectorStore vectorStore) {
    String sysPrompt = """
            您是一个数据产品的智能搜索引擎,负责根据用户输入的内容进行精准匹配、模糊匹配和近义词匹配,以搜索相关的数据记录。
            您只能搜索指定的内容,不能回复其他内容或添加解释。
            您可以通过[search_content]标识符来表示需要搜索的具体内容。要求您返回匹配内容的完整记录,以JSON数组格式呈现。
            如果搜索不到内容,请返回[no_data]。
            """;
    this.chatClient = builder
            .defaultSystem(sysPrompt)
            .defaultAdvisors(
                    new QuestionAnswerAdvisor(vectorStore, new SearchRequest())
            )
            .defaultOptions(
                    DashScopeChatOptions.builder()
                            .withModel("deepseek-r1")
                            .build()
            )
            .build();
}
  • 通过系统 Prompt 来指定智能体的能力;
  • 通过 QuestionAnswerAdvisor 绑定向量数据库。

4.3. 搜索接口

@GetMapping(value = "/search")
public List<SearchVo> search(@RequestParam String search, HttpServletResponse response) {
    response.setCharacterEncoding("UTF-8");
    PromptTemplate promptTemplate = new PromptTemplate("[search_content]: {search}");
    Prompt prompt = promptTemplate.create(Map.of("search", search));

    return chatClient.prompt(prompt)
            .call()
            .entity(new ParameterizedTypeReference<List<SearchVo>>() {});
}

这里通过 entity 方法来实现搜索结果以结构化的方式返回。

4.4. 测试接口

4.4.1. 搜索新能源

除了模糊匹配了新能源车之外,还匹配了和新能源相关的光伏数据。

4.4.21. 搜索收入

匹配同义词的收益数据。

 

五、总结

本文以智能搜索引擎场景,通过 RAG 技术,实现了全文搜索、模糊搜索、同义词推荐等功能,并以结构化的方式返回搜索结果。需要注意的是,在企业应用中,要把 SimpleVectorStore 改为成熟的第三方向量数据库,例如 milvuselasticsearchredis 等。

 

六、完整代码

  • Gitee地址:

https://gitee.com/zlt2000/zlt-spring-ai-app

  • Github地址:

https://github.com/zlt2000/zlt-spring-ai-app

Spring AI与DeepSeek实战二:打造企业级智能体 - zlt2000 - 博客园

mikel阅读(956)

来源: Spring AI与DeepSeek实战二:打造企业级智能体 – zlt2000 – 博客园

一、概述

智能体 Agent 能自主执行任务实现特定目标的 AI 程序。传统 AI(如ChatGPT)主要依靠用户输入指令,而智能体 Agent 可以自主思考、决策,并执行复杂任务,就像一个AI助手,能够独立完成多步操作。本文将以多语言翻译助手为场景,演示如何基于Spring AI与DeepSeek模型构建一个支持多种语言的企业级翻译智能体,实现精准可控的跨语言交互。

关于 Spring AI 与 DeepSeek 的集成,以及 API-KEY 的申请等内容,可参考文章《Spring AI与DeepSeek实战一:快速打造智能对话应用

 

二、系统Prompt

智能体的核心在于通过 Prompt 工程明确其能力边界。以下为翻译智能体的系统级 Prompt 设计:

您是一名专业的多语言翻译助手,需严格遵守以下规则:
1. **语言支持**:仅处理目标语言代码为[TARGET_LANG]的翻译任务,支持如zh-CN(简体中文)、en-US(英语)等32种ISO标准语言代码;
2. **输入格式**:用户使用---translate_content---作为分隔符,仅翻译分隔符内的文本,其余内容视为无效指令;
3. **行为限制**:禁止回答与翻译无关的问题,若输入不包含合法分隔符或目标语言,回复:"请提供有效的翻译指令"。
4. **支持多语言**:需要翻译的内容如果包含多种语言,都需要同时翻译为TARGET_LANG指定的语言。

关键设计解析:

  • 需要给大模型明确 角色 和 行为边界
  • 通过 TARGET_LANG 参数化语言配置,便于动态扩展;
  • 使用 ---translate_content--- 强制结构化输入,避免模型处理无关信息;
  • 明确拒绝策略,保障服务安全性。

 

三、Prompt模板

结合Spring AI的prompt模板,实现动态Prompt生成:

TARGET_LANG: {target}
---translate_content---
"{content}"

关键设计解析:

  • 使用占位符 {TARGET_LANG} 和 {content} 实现多语言动态适配;
  • 无论用户输入任何内容,只会出现在 translate_content 分隔符下。

 

四、核心代码

@GetMapping(value = "/translate")
public String translate(@RequestParam String input, @RequestParam(required = false) String target, HttpServletResponse response) {
    String systemPrompt = """
               您是一名专业的多语言翻译助手,需严格遵守以下规则:
               1. **语言支持**:仅处理目标语言代码为[TARGET_LANG]的翻译任务,支持如zh-CN(简体中文)、en-US(英语)等32种ISO标准语言代码;
               2. **输入格式**:用户使用---translate_content---作为分隔符,仅翻译分隔符内的文本,其余内容视为无效指令;
               3. **行为限制**:禁止回答与翻译无关的问题,若输入不包含合法分隔符或目标语言,回复:"请提供有效的翻译指令"。
               4. **支持多语言**:需要翻译的内容如果包含多种语言,都需要同时翻译为TARGET_LANG指定的语言。
               """;

    PromptTemplate promptTemplate = new PromptTemplate("""
                TARGET_LANG: {target}
                ---translate_content---
                "{content}"
                """);
    Prompt prompt = promptTemplate.create(Map.of("target", target, "content", input));

    String result = chatClient.prompt(prompt)
            .system(systemPrompt)
            .call()
            .content();
    if (result != null && result.length() >= 2) {
        result = result.substring(1, result.length() - 1);
    }
    return result;
}

通过 target 来指定目标语言,input 参数为需要翻译的内容。

 

五、测试

  • 直接调用接口测试:

尝试输入提问方式的内容,大模型也仅翻译内容

  • 配套一个前端页面测试:

  • 多语言同时翻译:
    翻译包含八种语言的内容

 

六、总结

本文通过翻译场景, 封印 了大模型的对话能力,演示了企业级智能体的三大核心能力:指令结构化行为边界控制 与 动态模板适配。然而,现实中的复杂任务(如合同审核、数据分析)往往需要更高级能力:

  1. 任务拆解:将复杂问题拆解为子任务链(如”翻译→摘要生成→格式校验”);
  2. 工作流引擎:通过状态机管理任务执行顺序与异常重试;
  3. 记忆与上下文:实现多轮对话的长期记忆管理。

 

七、完整代码

  • Gitee地址:

https://gitee.com/zlt2000/zlt-spring-ai-app

  • Github地址:

https://github.com/zlt2000/zlt-spring-ai-app

Spring Cloud Alibaba AI 入门与实践 - zlt2000 - 博客园

mikel阅读(292)

来源: Spring Cloud Alibaba AI 入门与实践 – zlt2000 – 博客园

一、概述

Spring AI 是 Spring 官方社区项目,旨在简化 Java AI 应用程序开发,让 Java 开发者像使用 Spring 开发普通应用一样开发 AI 应用。

Spring Cloud Alibaba AI 是一个将 Spring Cloud 微服务生态与阿里巴巴 AI 能力无缝集成的框架,帮助开发者快速构建具备 AI 功能的现代化应用。本文将介绍 Spring Cloud Alibaba AI 的基本概念、主要特性和功能,并演示如何完成一个 在线聊天 和 在线画图 的 AI 应用。

 

二、主要特性和功能

Spring Cloud Alibaba AI 目前基于 Spring AI 0.8.1 版本 API 完成通义系列大模型的接入。通义接入是基于阿里云 阿里云百炼 服务;而 阿里云百炼 建立在 模型即服务(MaaS) 的理念基础之上,围绕 AI 各领域模型,通过标准化的 API 提供包括模型推理、模型微调训练在内的多种模型服务。

主要提供以下核心功能:

2.1. 简单易用的集成

通过 Spring Boot 风格的自动配置机制,开发者只需少量代码配置,即可快速接入阿里云的 AI 服务。

2.2. 丰富的 AI 服务支持

支持以下核心能力:

  • 自然语言处理(NLP):文本分析、智能问答、翻译。
  • 计算机视觉(CV):图像生成、图像识别、目标检测。
  • 语音处理:语音识别、语音合成。
  • 数据分析与预测:数据建模、趋势分析。

2.3. 高度扩展性

通过配置中心和注册中心(如 Nacos)实现动态扩展,支持微服务架构的扩展需求。
提供接口定义,方便接入第三方 AI 平台。

 

三、构建 AI 应用

Spring Cloud Alibaba AI 对 Java 版本有要求,所以需要提前预装好 Java 17 环境。

3.1. 申请 API-KEY

登录阿里云,进入 阿里云百炼 的页面:

https://bailian.console.aliyun.com/?apiKey=1#/api-key

创建自己的 API-KEY

3.2. 添加依赖

在 Spring Boot 项目的 pom.xml 中添加 alibaba-ai 依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-ai</artifactId>
</dependency>

<repositories>
    <repository>
        <id>alimaven</id>
        <url>https://maven.aliyun.com/repository/public</url>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-snapshots</id>
        <url>https://repo.spring.io/snapshot</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

 

3.3. 配置 API-KEY

在 application.yml 中配置 Kafka 的相关属性,包括服务器地址、认证信息等。

spring:
  cloud:
    ai:
      tongyi:
        connection:
          api-key: sk-xxxxxx
  • api-key 配置在阿里云百炼里申请的api-key

3.4. 创建模型调用服务

@Service
@Slf4j
public class TongYiSimpleService {
    @Resource
    private TongYiChatModel chatClient;
    @Resource
    private TongYiImagesModel imageClient;

    public String chat(String message) {
        Prompt prompt = new Prompt(new UserMessage(message));
        return chatClient.call(prompt).getResult().getOutput().getContent();
    }

    public String image(String message) {
        ImagePrompt prompt = new ImagePrompt(message);
        Image image = imageClient.call(prompt).getResult().getOutput();
        return image.getB64Json();
    }
}

聊天和图片的服务,分别通过注入 TongYiChatModel 和 TongYiImagesModel 对象来实现,屏蔽底层通义大模型交互细节。

3.5. 创建controller

@RestController
@RequestMapping("/ai")
public class TongYiController {
    @Resource
    private TongYiSimpleService tongYiSimpleService;

    @GetMapping("/chat")
    public String chat(@RequestParam(value = "message") String message) {
        return tongYiSimpleService.chat(message);
    }

    @GetMapping("/image")
    public ResponseEntity<byte[]> image(@RequestParam(value = "message") String message) {
        String b64Str = tongYiSimpleService.image(message);
        byte[] imageBytes = Base64.getDecoder().decode(b64Str);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.IMAGE_JPEG);
        return new ResponseEntity<>(imageBytes, headers, HttpStatus.OK);
    }
}

3.6. 测试效果

3.6.1. 聊天接口

在浏览器输入:http://localhost:8009/ai/chat?message=你是谁

3.6.2. 图片接口

在浏览器输入:http://localhost:8009/ai/image?message=意大利面拌42号混凝土

3.6.3. 搭配聊天页面

四、总结

当前版本的 Spring Cloud Alibaba AI 主要完成了几种常见生成式模型的适配,涵盖对话、文生图、文生语音等。在未来的版本中将继续推进 VectorStoreEmbeddingETL PipelineRAG 等更多 AI 应用开发场景的建设。

完整的样例代码下载:
https://gitee.com/zlt2000/spring-cloud-ai-sample

MCP开发应用,使用python部署sse模式 - 肖祥 - 博客园

mikel阅读(364)

来源: MCP开发应用,使用python部署sse模式 – 肖祥 – 博客园

一、概述

MCP服务端当前支持两种与客户端的数据通信方式:标准输入输出(stdio)  和 基于Http的服务器推送事件(http sse)

1.1 标准输入输出(stdio)

原理:  标准输入输出是一种用于本地通信的传输方式。在这种模式下,MCP 客户端会将服务器程序作为子进程启动,双方通过约定的标准输入和标准输出(可能是通过共享文件等方法)进行数据交换。具体而言,客户端通过标准输入发送请求,服务器通过标准输出返回响应。。

适用场景:  标准输入输出方式适用于客户端和服务器在同一台机器上运行的场景(本地自行编写服务端或将别人编写的服务端代码pull到本地执行),确保了高效、低延迟的通信。这种直接的数据传输方式减少了网络延迟和传输开销,适合需要快速响应的本地应用。

1.2 基于Http的服务器推送事件(http sse)

原理:  客户端和服务端通过 HTTP 协议进行通信,利用 SSE 实现服务端向客户端的实时数据推送,服务端定义了/see与/messages接口用于推送与接收数据。这里要注意SSE协议和WebSocket协议的区别,SSE协议是单向的,客户端和服务端建立连接后,只能由服务端向客户端进行消息推送。而WebSocket协议客户端和服务端建立连接后,客户端可以通过send向服务端发送数据,并通过onmessage事件接收服务端传过来的数据。

适用场景:  适用于客户端和服务端位于不同物理位置的场景,尤其是对于分布式或远程部署的场景,基于 HTTP 和 SSE 的传输方式更合适。

二、MCP开发应用

MCP Server应用平台

主要有以下这些:

7.MCP.SO

这里面有很多已经开发好的mcp应用,可以拿来直接使用即可。
Cherry Studio客户端推荐的mcp平台为:https://mcp.so
但是大家可以发现一个问题,就是MCP Server应用平台,发布的应用,80%以上,运行方式都是stdio,这种方式适合本地开发。
但是想在dify里面使用,一般都是sse方式,因为适合远程调用。即使是不同命名空间,不同的工作流依然可以调用,只要网络能联通,就没问题。
而且sse方式,只需要运行一次,就可以让成千上万个dify应用调用,还是很方便的。

MCP应用开发

以mySQL为例子,进入网站https://mcp.so,搜索mySQL,找到MySQL_mcp_server_pro,链接如下:https://mcp.so/server/mysql_mcp_server_pro/wenb1n-dev(xwb602625136)?tab=content
这个应用只支持mysql 5.6,由于我的mysql版本是8.0,需要修改对应的源码才行。
新建空目录Mysql_mcp_server_pro,新建2个文件.evn,server.py
.env文件内容如下:
复制代码
# MySQL数据库配置
MYSQL_HOST=192.168.20.128
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=abcd@1234
MYSQL_DATABASE=test
复制代码

这个是mysql连接信息

 

server.py

复制代码
import os

import uvicorn
from mcp.server.sse import SseServerTransport
from mysql.connector import connect, Error
from mcp.server import Server
from mcp.types import Tool, TextContent
from starlette.applications import Starlette
from starlette.routing import Route, Mount
from dotenv import load_dotenv


def get_db_config():
    """从环境变量获取数据库配置信息

    返回:
        dict: 包含数据库连接所需的配置信息
        - host: 数据库主机地址
        - port: 数据库端口
        - user: 数据库用户名
        - password: 数据库密码
        - database: 数据库名称

    异常:
        ValueError: 当必需的配置信息缺失时抛出
    """

    # 加载.env文件
    load_dotenv()

    config = {
        "host": os.getenv("MYSQL_HOST", "localhost"),
        "port": int(os.getenv("MYSQL_PORT", "3306")),
        "user": os.getenv("MYSQL_USER"),
        "password": os.getenv("MYSQL_PASSWORD"),
        "database": os.getenv("MYSQL_DATABASE"),
    }
    print(config)
    if not all([config["user"], config["password"], config["database"]]):
        raise ValueError("缺少必需的数据库配置")

    return config


def execute_sql(query: str) -> list[TextContent]:
    """执行SQL查询语句

    参数:
        query (str): 要执行的SQL语句,支持多条语句以分号分隔

    返回:
        list[TextContent]: 包含查询结果的TextContent列表
        - 对于SELECT查询:返回CSV格式的结果,包含列名和数据
        - 对于SHOW TABLES:返回数据库中的所有表名
        - 对于其他查询:返回执行状态和影响行数
        - 多条语句的结果以"---"分隔

    异常:
        Error: 当数据库连接或查询执行失败时抛出
    """
    config = get_db_config()
    try:
        with connect(**config) as conn:
            with conn.cursor() as cursor:
                statements = [stmt.strip() for stmt in query.split(";") if stmt.strip()]
                results = []

                for statement in statements:
                    try:
                        cursor.execute(statement)

                        # 检查语句是否返回了结果集 (SELECT, SHOW, EXPLAIN, etc.)
                        if cursor.description:
                            columns = [desc[0] for desc in cursor.description]
                            rows = cursor.fetchall()

                            # 将每一行的数据转换为字符串,特殊处理None值
                            formatted_rows = []
                            for row in rows:
                                formatted_row = [
                                    "NULL" if value is None else str(value)
                                    for value in row
                                ]
                                formatted_rows.append(",".join(formatted_row))

                            # 将列名和数据合并为CSV格式
                            results.append(
                                "\n".join([",".join(columns)] + formatted_rows)
                            )

                        # 如果语句没有返回结果集 (INSERT, UPDATE, DELETE, etc.)
                        else:
                            conn.commit()  # 只有在非查询语句时才提交
                            results.append(f"查询执行成功。影响行数: {cursor.rowcount}")

                    except Error as stmt_error:
                        # 单条语句执行出错时,记录错误并继续执行
                        results.append(
                            f"执行语句 '{statement}' 出错: {str(stmt_error)}"
                        )
                        # 可以在这里选择是否继续执行后续语句,目前是继续

                return [TextContent(type="text", text="\n---\n".join(results))]

    except Error as e:
        print(f"执行SQL '{query}' 时出错: {e}")
        return [TextContent(type="text", text=f"执行查询时出错: {str(e)}")]


def get_table_name(text: str) -> list[TextContent]:
    """根据表的中文注释搜索数据库中的表名

    参数:
        text (str): 要搜索的表中文注释关键词

    返回:
        list[TextContent]: 包含查询结果的TextContent列表
        - 返回匹配的表名、数据库名和表注释信息
        - 结果以CSV格式返回,包含列名和数据
    """
    config = get_db_config()
    sql = "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COMMENT "
    sql += f"FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{config['database']}' AND TABLE_COMMENT LIKE '%{text}%';"
    return execute_sql(sql)


def get_table_desc(text: str) -> list[TextContent]:
    """获取指定表的字段结构信息

    参数:
        text (str): 要查询的表名,多个表名以逗号分隔

    返回:
        list[TextContent]: 包含查询结果的TextContent列表
        - 返回表的字段名、字段注释等信息
        - 结果按表名和字段顺序排序
        - 结果以CSV格式返回,包含列名和数据
    """
    config = get_db_config()
    # 将输入的表名按逗号分割成列表
    table_names = [name.strip() for name in text.split(",")]
    # 构建IN条件
    table_condition = "','".join(table_names)
    sql = "SELECT TABLE_NAME, COLUMN_NAME, COLUMN_COMMENT "
    sql += (
        f"FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '{config['database']}' "
    )
    sql += f"AND TABLE_NAME IN ('{table_condition}') ORDER BY TABLE_NAME, ORDINAL_POSITION;"
    return execute_sql(sql)

def get_lock_tables() -> list[TextContent]:
    sql = """SELECT
    p2.`HOST` AS 被阻塞方host,
    p2.`USER` AS 被阻塞方用户,
    r.trx_id AS 被阻塞方事务id,
    r.trx_mysql_thread_id AS 被阻塞方线程号,
    TIMESTAMPDIFF(SECOND, r.trx_wait_started, CURRENT_TIMESTAMP) AS 等待时间,
    r.trx_query AS 被阻塞的查询,
    l.OBJECT_NAME AS 阻塞方锁住的表,
    m.LOCK_MODE AS 被阻塞方的锁模式,
    m.LOCK_TYPE AS '被阻塞方的锁类型(表锁还是行锁)',
    m.INDEX_NAME AS 被阻塞方锁住的索引,
    m.OBJECT_SCHEMA AS 被阻塞方锁对象的数据库名,
    m.OBJECT_NAME AS 被阻塞方锁对象的表名,
    m.LOCK_DATA AS 被阻塞方事务锁定记录的主键值,
    p.`HOST` AS 阻塞方主机,
    p.`USER` AS 阻塞方用户,
    b.trx_id AS 阻塞方事务id,
    b.trx_mysql_thread_id AS 阻塞方线程号,
    b.trx_query AS 阻塞方查询,
    l.LOCK_MODE AS 阻塞方的锁模式,
    l.LOCK_TYPE AS '阻塞方的锁类型(表锁还是行锁)',
    l.INDEX_NAME AS 阻塞方锁住的索引,
    l.OBJECT_SCHEMA AS 阻塞方锁对象的数据库名,
    l.OBJECT_NAME AS 阻塞方锁对象的表名,
    l.LOCK_DATA AS 阻塞方事务锁定记录的主键值,
    IF(p.COMMAND = 'Sleep', CONCAT(p.TIME, ' 秒'), 0) AS 阻塞方事务空闲的时间
    FROM performance_schema.data_lock_waits w
    INNER JOIN performance_schema.data_locks l ON w.BLOCKING_ENGINE_LOCK_ID = l.ENGINE_LOCK_ID
    INNER JOIN performance_schema.data_locks m ON w.REQUESTING_ENGINE_LOCK_ID = m.ENGINE_LOCK_ID
    INNER JOIN information_schema.INNODB_TRX b ON b.trx_id = w.BLOCKING_ENGINE_TRANSACTION_ID
    INNER JOIN information_schema.INNODB_TRX r ON r.trx_id = w.REQUESTING_ENGINE_TRANSACTION_ID
    INNER JOIN information_schema.PROCESSLIST p ON p.ID = b.trx_mysql_thread_id
    INNER JOIN information_schema.PROCESSLIST p2 ON p2.ID = r.trx_mysql_thread_id
    ORDER BY 等待时间 DESC;"""

    return execute_sql(sql)


# 初始化服务器
app = Server("operateMysql")


@app.list_tools()
async def list_tools() -> list[Tool]:
    """列出可用的MySQL工具

    返回:
        list[Tool]: 工具列表,当前仅包含execute_sql工具
    """
    return [
        Tool(
            name="execute_sql",
            description="在MySQL8.0数据库上执行SQL",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "要执行的SQL语句"}
                },
                "required": ["query"],
            },
        ),
        Tool(
            name="get_table_name",
            description="根据表中文名搜索数据库中对应的表名",
            inputSchema={
                "type": "object",
                "properties": {
                    "text": {"type": "string", "description": "要搜索的表中文名"}
                },
                "required": ["text"],
            },
        ),
        Tool(
            name="get_table_desc",
            description="根据表名搜索数据库中对应的表结构,支持多表查询",
            inputSchema={
                "type": "object",
                "properties": {
                    "text": {"type": "string", "description": "要搜索的表名"}
                },
                "required": ["text"],
            },
        ),
        Tool(
            name="get_lock_tables",
            description="获取当前mysql服务器InnoDB 的行级锁",
            inputSchema={"type": "object", "properties": {}},
        ),
    ]


@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:

    if name == "execute_sql":
        query = arguments.get("query")
        if not query:
            raise ValueError("缺少查询语句")
        return execute_sql(query)
    elif name == "get_table_name":
        text = arguments.get("text")
        if not text:
            raise ValueError("缺少表信息")
        return get_table_name(text)
    elif name == "get_table_desc":
        text = arguments.get("text")
        if not text:
            raise ValueError("缺少表信息")
        return get_table_desc(text)
    elif name == "get_lock_tables":
        return get_lock_tables()

    raise ValueError(f"未知的工具: {name}")


sse = SseServerTransport("/messages/")


# Handler for SSE connections
async def handle_sse(request):
    async with sse.connect_sse(
        request.scope, request.receive, request._send
    ) as streams:
        await app.run(streams[0], streams[1], app.create_initialization_options())


# Create Starlette app with routes
starlette_app = Starlette(
    debug=True,
    routes=[
        Route("/sse", endpoint=handle_sse),
        Mount("/messages/", app=sse.handle_post_message),
    ],
)


if __name__ == "__main__":
    uvicorn.run(starlette_app, host="0.0.0.0", port=9000)
复制代码

这里面,主要提供了4个工具方法,分别是:

execute_sql
get_table_name
get_table_desc
get_lock_tables

 

安装python依赖

pip install mcp
pip install mysql-connector-python
pip install uvicorn
pip install python-dotenv
pip install starlette

 

启动应用

python server.py

输出:

INFO:     Started server process [23756]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:9000 (Press CTRL+C to quit)
INFO:     127.0.0.1:60896 - "GET /sse HTTP/1.1" 200 OK

 

访问页面:http://127.0.0.1:9000/sse

效果如下:

 

三、测试MCP应用

客户端添加MCP

以Cherry Studio客户端为例子,注意:必须是Cherry Studio最新版本,才有MCP设置。

添加MCP服务器

输入名称:mysql_mcp_server_pro

类型:sse

URL:http://127.0.0.1:9000/sse

 

点击保存

 

保存成功后,就可以看到工具列表了,只有4个

测试MCP应用

返回主页,点击新建助手,选择模型

在输入框,找到MCP服务器

开启MCP

 

先来看mysql的数据表score,内容如下:

 

 查询成绩表score的内容

 查询成绩表的表名是什么

 

查询成绩表的表结构

 

获取当前mysql的锁

 

总结一下,AI模型调用MCP,还是很方便的。

有些时候AI模型做不到的,你可以自己写一个MCP应用。比如上面提到的查询mysql表数据,还有很多呢。

比如查询内部CRM系统,gitlab信息,内部业务系统,处理特定格式excel文件等等,都可以的。

AI作曲神器加持!零基础也能玩转音乐副业,解锁你的创作新技能! - 国外网赚博客

mikel阅读(246)

来源: AI作曲神器加持!零基础也能玩转音乐副业,解锁你的创作新技能! – 国外网赚博客

最近有个朋友跟我吐槽:“我这辈子连五线谱都看不懂,居然也能靠写歌赚钱?” 我听完差点笑出声——这年头,AI连音乐创作的活儿都能包了!只要你会打字、会点鼠标,分分钟就能搞出一首原创歌曲,还能挂在网上卖钱。今天咱们就来聊聊,怎么用AI工具把“音乐创作”这个高大上的事儿,变成人人能上手的“副业神器”!

AI作曲神器加持!零基础也能玩转音乐副业,解锁你的创作新技能!

一、音乐小白逆袭指南:AI工具到底有多“无脑”?

以前写首歌有多难?你得会写词、懂编曲、能哼旋律,搞不好还得学个乐器。但现在,AI直接把门槛踩成了平地!

1. 歌词生成:让ChatGPT当你的“作词小弟”

别担心自己文笔差,直接把要求丢给AI:“来首失恋风格的R&B,歌词要有地铁、咖啡、下雨天这些元素!” 不到10秒,AI就能给你整出一篇堪比青春疼痛文学的歌词。

比如这样:

“午夜的地铁站台,咖啡凉了心事还在翻涌,雨滴敲打玻璃,像你最后那句‘保重’……”

(别问我为啥这么熟练,这玩意儿试多了会上瘾!)

2. 一键生成歌曲:比点外卖还简单

有了歌词,直接打开Suno这类AI音乐平台,把文字粘贴进去,选个风格——流行、民谣、电子甚至古风都能搞定。点击“生成”,等个两三分钟,一首带旋律、伴奏甚至人声的完整歌曲就出炉了!

有个哥们儿用这方法,给自家宠物狗写了首生日歌发到抖音,居然火了!评论区全是“求定制”:“给我家猫也整一首!”“公司年会缺主题曲,老板说预算500!”

二、赚钱路子野得很:你的歌能卖给谁?

路子1:定制化“卖歌”

婚庆公司:新人想要专属婚礼BGM?报价800-2000元,把“新郎新娘名字”嵌进歌词,甜蜜度直接拉满!

小微企业:很多小店需要宣传曲,比如奶茶店想要“芋泥波波之歌”,火锅店需要“麻辣狂欢进行曲”。

短视频博主:搞笑博主需要魔性BGM,知识博主需要舒缓背景音乐,需求量超大!

路子2:平台流量分成

把AI生成的歌曲配上简单动画(用剪映就能搞定),上传到抖音、B站、YouTube。有个小姐姐专做“古风AI歌曲”,每条视频挂上“原创音乐人”标签,靠播放量分成月入四位数,还攒了一波铁粉!

路子3:直播现场“接单”

开个直播间,标题写“在线写歌!弹幕点歌风格立马出”!观众刷个礼物就能点歌,比如:“失恋emo版《挖呀挖》”“老板diss版《孤勇者》”。有人靠这招一晚上收打赏赚了2000+,顺便还引流到私域接长期订单。

三、实战操作手册:从0到1做出第一首“爆款”

Step1:歌词生成“三板斧”

场景化:别写“我爱你你爱他”,换成“加班到凌晨三点,便利店关东煮是唯一的温暖”。

押韵工具:用“押韵助手”App检查韵脚,避免出现“红烧肉押韵小怪兽”的惨案。

热词蹭流量:比如最近流行的“脆皮大学生”“全职儿女”,把这些梗塞进歌词里,自带传播属性!

Step2:让AI唱出“人味儿”

很多人吐槽AI唱歌像机器人,其实有个绝招:在Suno里选择“带情绪的人声”,比如“慵懒女声”“沙哑大叔音”。再调整播放速度,加快0.1倍速能让人声更自然,亲测有效!

Step3:MV制作“傻瓜攻略”

用剪映的“图文成片”功能,导入歌词自动匹配素材。重点来了:把AI生成的歌曲和画面节奏对齐!比如唱到“心跳加速”时切个赛车镜头,唱到“月光”时放夜景,瞬间高大上!

四、避坑指南:这些雷区千万别踩!

雷区1:版权问题

用AI工具前,务必看清用户协议!有些平台规定“生成的音乐版权归平台所有”,这种千万别碰。推荐Suno、Mureka这类明确“版权归创作者”的工具,避免辛苦做的歌被人白嫖。

雷区2:盲目追求数量

有人一天狂发20首歌,结果平台判定“低质内容”直接限流。记住:一周发3-5首精品,比每天水10首更有用!

雷区3:忽视“人设包装”

别光发歌!在主页写个“野生音乐人成长日记”,晒晒创作过程:“今天用AI给楼下煎饼摊写了主题曲,阿姨送我加蛋煎饼!” 接地气的人设能让粉丝黏性暴增!

五、进阶秘籍:怎么让收益翻倍?

秘籍1:打造“套餐服务”

比如定价299元套餐包含:“定制歌曲+15秒短视频+抖音热门标签优化”。客户觉得超值,你还能多赚附加服务费!

秘籍2:跨界合作

联系短视频编剧、广告公司,提供“音乐+文案”打包服务。有个团队靠给电商广告配魔性BGM,单条报价5000+!

秘籍3:卖“创作模板”

把爆款歌曲的歌词结构、旋律套路整理成模板,挂在知识付费平台卖。比如“7天学会AI写歌!附赠100个热门选题库!” 妥妥的睡后收入!

六、音乐不止是梦想,还能是“饭票”

有人问我:“用AI做音乐,算不算投机取巧?” 我反手就甩了个例子:19世纪照相术刚发明时,画家们也觉得“这玩意侮辱艺术”,但现在谁不夸摄影是门技术?AI时代,咱们比的不是“谁更苦哈哈”,而是“谁更会借力”。

再说了,靠AI写歌赚钱不丢人!客户要的是“走心的作品”,又不是“痛苦的艺术家”。你负责提供创意和审美,AI负责技术执行,这明明就是双赢!

别等灵感,要去制造灵感;别等机会,要去创造机会。 打开电脑,选个AI工具,今晚就写出你的第一首“致富神曲”吧!

阅读全文

C# LINQ 快速入门实战指南,建议收藏学习! - 追逐时光者 - 博客园

mikel阅读(469)

来源: C# LINQ 快速入门实战指南,建议收藏学习! – 追逐时光者 – 博客园

C# LINQ 快速入门实战指南,建议收藏学习!

前言

因为咱们的.NET EF Core快速入门实战教程经常会用到 LINQ 去查询和操作 MySQL 中的数据,因此我觉得很有必要对 LINQ 的一些使用技巧、常用方法、特性做一个详细的介绍,让大家在后续的课程中不迷茫。

LINQ介绍

LINQ语言集成查询是一系列直接将查询功能集成到 C# 语言的技术统称。数据查询历来都表示为简单的字符串,没有编译时类型检查或 IntelliSense 支持。此外,需要针对每种类型的数据源了解不同的查询语言:SQL 数据库、XML 文档、各种 Web 服务等。然而,LINQ的出现改变了这一现状,它使查询成为了与类、方法和事件同等重要的高级语言构造。通过LINQ,开发者能够以声明性的方式查询和操作数据,极大地提高了开发效率和代码的可维护性。

LINQ具有以下特性

  • 强类型:编译时验证查询逻辑,减少运行时错误。
  • 延迟执行:LINQ查询通常是延迟执行的,即查询表达式本身不会立即执行,直到实际遍历结果时才触发查询。使用 ToList()ToArray()ToDictionary()FirstOrDefault()等方法可立即执行。
  • 支持多种数据源:LINQ可以用于查询多种数据源,如LINQ to Objects、LINQ to XML、LINQ to SQL、LINQ to Entities(Entity Framework)等。

LINQ中常用方法

操作示例数据

        public class StudentInfo
        {
            public int StudentID { get; set; }
            public string StudentName { get; set; }
            public DateTime Birthday { get; set; }
            public int ClassID { get; set; }
            public string Address { get; set; }
            public List<Course> Courses { get; set; } = new List<Course>();
        }

        public class Course
        {
            public int CourseID { get; set; }
            public string CourseName { get; set; }
        }

        static List<StudentInfo> students = new List<StudentInfo>
        {
            new StudentInfo
            {
                StudentID=1,
                StudentName="大姚",
                Birthday=Convert.ToDateTime("1997-10-25"),
                ClassID=101,
                Courses = new List<Course>
                {
                    new Course { CourseID = 101, CourseName = "语文" },
                    new Course { CourseID = 102, CourseName = "数学" }
                }
            },
            new StudentInfo
            {
                StudentID=2,
                StudentName="李四",
                Birthday=Convert.ToDateTime("1998-10-25"),
                ClassID=101,
                Courses = new List<Course>
                {
                    new Course { CourseID = 101, CourseName = "语文" },
                    new Course { CourseID = 102, CourseName = "数学" }
                }
            },
            new StudentInfo
            {
                StudentID=3,
                StudentName="王五",
                Birthday=Convert.ToDateTime("1999-10-25"),
                ClassID=102,
                Address="广州",
                Courses = new List<Course>
                {
                    new Course { CourseID = 101, CourseName = "语文" },
                    new Course { CourseID = 102, CourseName = "数学" }
                }
            },
            new StudentInfo
            {
                StudentID=4,
                StudentName="时光者",
                Birthday=Convert.ToDateTime("1999-11-25"),
                ClassID=102,
                Address="深圳" ,
                Courses = new List<Course>
                {
                    new Course { CourseID = 104, CourseName = "历史" },
                    new Course { CourseID = 103, CourseName = "地理" }
                }
            }
        };

基本查询方法

  • Where:用于过滤集合中的元素。
  • Select:用于将集合中的每个元素投影(转换)为新形式。
  • SelectMany:用于将多个集合展平为一个集合。
            var femaleStudents = students.Where(s => s.StudentName == "时光者");
            var studentNames = students.Select(s => s.StudentName);

            // 使用SelectMany展平所有学生的课程列表
            var allCourses = students.SelectMany(student => student.Courses).ToList();

            // 输出所有课程的名称
            foreach (var course in allCourses)
            {
                Console.WriteLine(course.CourseName);
            }

转换方法

  • ToList:将实现了IEnumerable<T>接口的集合转换为一个List<T>类型的对象,属于将集合转换为特定类型列表的方法。
  • ToArray:将一个实现了IEnumerable<T>接口的集合转换为一个数组,属于将集合转换为数组类型的方法。
  • ToDictionary:将一个集合转换为一个字典(Dictionary<TKey, TValue>),其中集合的元素作为字典的键或值(通过提供的键选择器和值选择器函数)。属于将集合转换为键值对集合(字典)的方法。
  • ToLookup:将一个序列分组并返回一个ILookup<TKey, TElement>对象,这是一个one-to-many集合,即一个键可以对应多个值。属于将集合转换为分组集合(查找表)的方法。
            var studentList = students.ToList();
            var studentArray = students.ToArray();
            var studentDictionary = students.ToDictionary(s => s.StudentID, s => s.StudentName);
            var studentLookup = students.ToLookup(s => s.ClassID, s => s.StudentName);

元素操作方法

  • First:返回集合中的第一个元素。
  • FirstOrDefault:返回集合中的第一个元素,如果集合为空则返回默认值。
  • Single:返回集合中的单个元素,如果集合为空或包含多个元素则抛出异常。
  • SingleOrDefault:返回集合中的单个元素,如果集合为空或包含多个元素则返回默认值。
  • Last:返回集合中的最后一个元素。
  • LastOrDefault:返回集合中的最后一个元素,如果集合为空则返回默认值。
  • ElementAt:返回集合中指定索引处的元素。
  • ElementAtOrDefault:返回集合中指定索引处的元素,如果索引超出范围则返回默认值。
  • DefaultIfEmpty:如果集合为空,则返回一个包含默认值的集合。
            var firstStudent = students.First();
            var firstAdult = students.FirstOrDefault(s => s.Birthday <= DateTime.Now.AddYears(-18));
            var onlyWangWu = students.Single(s => s.StudentName == "王五");
            var wangWuOrDefault = students.SingleOrDefault(s => s.StudentName == "王六");
            var lastStudent = students.Last();
            var lastAdult = students.LastOrDefault(s => s.Birthday <= DateTime.Now.AddYears(-18));
            var secondStudent = students.ElementAt(1);
            var tenthStudentOrDefault = students.ElementAtOrDefault(9);
            var nonEmptyStudents = students.DefaultIfEmpty(new StudentInfo { StudentID = 0, StudentName = "默认Student", Address = "默认" });

排序方法

  • OrderBy:用于对集合进行升序排序。
  • OrderByDescending:用于对集合进行降序排序。
  • ThenBy:用于在已排序的集合上应用次要排序。
  • ThenByDescending:用于在已排序的集合上应用次要降序排序。
            var sortedByBirthdayAsc = students.OrderBy(s => s.Birthday);
            var sortedByClassIDDesc = students.OrderByDescending(s => s.ClassID);
            var sortedByNameThenClassID = students.OrderBy(s => s.StudentName).ThenBy(s => s.ClassID);
            var sortedThenByDescending = students.OrderBy(s => s.StudentName).ThenBy(s => s.ClassID).ThenByDescending(x => x.Birthday);

聚合方法

  • Count:返回集合中的元素数量。
  • Sum:返回集合中数值类型元素的和。
  • Average:返回集合中数值类型元素的平均值。
  • Min:返回集合中的最小值。
  • Max:返回集合中的最大值。
  • Aggregate:对集合进行自定义聚合操作。
            int studentCount = students.Count();
            int totalClassID = students.Sum(s => s.ClassID);
            double averageAge = students.Average(s => DateTime.Now.Year - s.Birthday.Year);
            int minClassID = students.Min(s => s.ClassID);
            int maxClassID = students.Max(s => s.ClassID);
            string concatenatedNames = students.Aggregate("", (acc, s) => acc == "" ? s.StudentName : acc + ", " + s.StudentName);

集合操作方法

  • Distinct:返回集合中的唯一元素。
  • Union:返回两个集合的并集。
  • Intersect:返回两个集合的交集。
  • Except:返回在第一个集合中存在但不在第二个集合中存在的元素。
  • Concat:连接两个或多个集合,并返回一个新的序列。
            var uniqueClassIDs = students.Select(s => s.ClassID).Distinct();
            var unionClassIDs = uniqueClassIDs.Union(new[] { 103, 104 });
            var intersectClassIDs = uniqueClassIDs.Intersect(new[] { 101, 103 });
            var exceptClassIDs = uniqueClassIDs.Except(new[] { 101 });
            var concatClassIDs = uniqueClassIDs.Concat(new[] { 103, 104 });

分组与连接方法

  • GroupBy:根据键对集合进行多级分组。
  • Join:根据匹配键连接两个集合。
  var groupedByClassID = students.GroupBy(s => s.ClassID);
  var otherStudent = new List<StudentInfo>
  {
     new StudentInfo
     {
         StudentID=4,
         StudentName="摇一摇",
         Birthday=Convert.ToDateTime("2997-10-25"),
         ClassID=101,
         Courses = new List<Course>
         {
             new Course { CourseID = 101, CourseName = "语文" },
             new Course { CourseID = 102, CourseName = "数学" }
         }
     }
  };
  var listJoin = students.Join(
      otherStudent, // 要连接的第二个序列
      s1 => s1.StudentID, // 从第一个序列中提取键
      s2 => s2.StudentID, // 从第二个序列中提取键
      (s1, s2) => new // 结果选择器,指定如何从两个匹配元素创建结果
      {
          StudentID = s1.StudentID,
          StudentName = s1.StudentName,
          Birthday = s1.Birthday,
          ClassID = s1.ClassID,
          Address = s1.Address,
          Courses = s1.Courses,
          OtherStudentName = s2.StudentName //假设我们想要包含第二个序列中学生的名称
      });

跳过与获取指定数量的元素(常用作分页)

  • Skip:用于跳过集合中指定数量的元素,并返回剩余的元素序列。
  • Take:用于从集合的开头获取指定数量的元素,并返回一个新的序列。
            var skippedStudents = students.Skip(1);
            var takenStudents = students.Take(2);

            //数据分页查询(Skip + Take)
            int pageNumber = 2;
            int pageSize = 10;
            var pagedUsers = skippedStudents
                .OrderBy(u => u.ClassID) // 必须排序
                .Skip((pageNumber - 1) * pageSize)
                .Take(pageSize)
                .ToList();

条件判断方法

  • All:用于判断集合中的所有元素是否都满足指定条件。
  • Any:检查集合是否包含任何元素,或用于判断集合中是否存在至少一个满足指定条件的元素。
  • Contains:用于判断集合中是否包含指定的元素。
            bool allAdults = students.All(s => s.Birthday <= DateTime.Now.AddYears(-18));
            bool anyAdults = students.Any(s => s.Birthday <= DateTime.Now.AddYears(-18));
            bool containsWangWu = students.Contains(students.First(s => s.StudentName == "王五"));

更多方法查询

 

查询语法

LINQ提供了类似于SQL的查询语法,允许开发者以几乎相同的方式对不同类型的数据源进行查询。查询语法使用from、where、select、orderby等关键字。

            var querySyntaxResult = from student in students
                                    where student.ClassID == 101
                                    orderby student.StudentName ascending
                                    select student;

            Console.WriteLine("查询语法结果:");
            foreach (var student in querySyntaxResult)
            {
                Console.WriteLine($"{student.StudentName}, ClassID: {student.ClassID}");
            }

查询关键字:

  • from: 指定数据源和范围变量(类似于迭代变量)。
  • where: 基于由逻辑 AND 和 OR 运算符(&& 或 ||)分隔的一个或多个布尔表达式筛选源元素。
  • select: 指定执行查询时,所返回序列中元素的类型和形状。
  • group: 根据指定的密钥值对查询结果分组。
  • into: 提供可作为对 join、group 或 select 子句结果引用的标识符(简单理解用于将配对的结果收集到一个临时序列)。
  • orderby: 根据元素类型的默认比较器对查询结果进行升序或降序排序。
  • join: 基于两个指定匹配条件间的相等比较而联接两个数据源(简单理解根据指定的键将两个序列中的元素配对)。
  • let: 引入范围变量,在查询表达式中存储子表达式结果。
  • in: join子句中的上下文关键字。
  • on: join子句中的上下文关键字。
  • equals: join子句中的上下文关键字。
  • by: group 子句中的上下文关键字。
  • ascending: orderby子句中的上下文关键字。
  • descending: orderby子句中的上下文关键字。

方法语法

方法语法也称为扩展方法语法,使用点号“.”和一系列扩展方法来构建查询。

            var methodSyntaxResult = students
                                    .Where(student => student.ClassID == 101)
                                    .OrderBy(student => student.StudentName)
                                    .ToList();


            Console.WriteLine("方法语法结果:");
            foreach (var student in methodSyntaxResult)
            {
                Console.WriteLine($"{student.StudentName}, ClassID: {student.ClassID}");
            }

混合查询和方法语法

            var mixedResult = (from student in students
                               where student.ClassID == 101
                               where student.Courses.Any(course => course.CourseName == "数学")
                               orderby student.StudentName ascending
                               select student)
                       .Take(2)
                       .ToList();

            // 输出结果
            Console.WriteLine("混合查询结果:");
            foreach (var student in mixedResult)
            {
                Console.WriteLine($"{student.StudentName}, ClassID: {student.ClassID}");
            }

参考文章

openglasses项目软件代码改造 - 知乎

mikel阅读(243)

来源: openglasses项目软件代码改造 – 知乎

一、项目地址

github.com/BasedHardwar

二、代码成分

分硬件+软件两部分

三、项目介绍

这个项目是使用seeed studio XIAO esp32S3 sense这块开发版作为蓝牙模块(释放的蓝牙不是平时手机能搜到的那种蓝牙)

1、然后适配硬件代码,可以支持每N秒拍一次照片;

2、在软件项目中支持对每次拍到的照片进行【图片解释】,以及可以进行对话并使用历史所有的图片解释内容进行回答(回答不仅有文本,还有语音播放)

四、在网上这个项目的如何搭建的文档有很多,这里列举几个

我用OpenGlass做了一个AI眼镜【上篇】 | Rocket Lu

mp.weixin.qq.com/mp/wap

五分钟——配置OpenGlass的硬件

五、源码改造

这篇文章主要是把作者在修改软件代码时的一些源码进行分享,因为软件代码中有地方用到ollmagropopenai,这些直接用起来都不是很方便,比如ollma需要在本地进行安装,需要电脑的配置很高;grop是国外的;openai直接无法访问

1、用ollma进行图像解释,暂时只有两种方案,第一种是和原代码中一样,使用大模型来进行对话解释,国内暂时只有【阿里的通义千问】能做到这点;第二种是用传统的sdk/api方式,暂时只有【百度】可以做到这点,但是百度的接口有些麻烦,要连续调两个接口才行(第一个接口时返回一个reqid,第二个是用这个reqid来获取接口)

// 定义MessageContent接口
interface MessageContent {
    type: string;
    text?: string;
    image_url?: {
        url: string;
    };
}
// 定义Message接口
interface Message {
    role: string;
    content: MessageContent[];
}
export type KnownModelQianwen =
    | 'qwen-vl-max-latest'
    | 'qwen-vl-plus'
    | 'qwen-vl-max'

export async function qianwenInference(args: {
    model: string,
    messages: Message[]
}) {
    const token: string = keys.qianwen_key;
    const headers: { [key: string]: string } = {
        "Content-Type": "application/json", "Authorization": `Bearer ${token}`
    }

    const response = await backoff<any>(async () => {

        let converted: { role: string, content: MessageContent[] }[] = [];
        for (let message of args.messages) {
            console.log("message=", message)
            let content_tmp: MessageContent[] = []
            
            for (let content of message.content) {
                content_tmp.push({
                    type: content.type,
                    text: content.text ? trimIdent(content.text) : undefined,
                    image_url: content.image_url ? {
                        url: content.image_url.url
                    } : undefined
                })
            }
            converted.push({
                role: message.role,
                content: content_tmp
            });
        }
        const payload = {
            model: args.model,
            messages: converted,
        }
        let resp = await axios.post(keys.qianwen, payload, {
            headers: headers
        });
        // console.log("resp=",resp)
        return resp['data']["choices"][0]["message"]["content"];

    });
    return trimIdent(((response ?? '') as string));
}

2、grop的目的是单纯的大模型对话,原则上用哪个http api接口/sdk都可以。但是有个细节需要注意,这个细节就可以筛选掉一大批的选择,几乎所有的node.js对应的sdk都用到了node里面的util包,但是这个包在浏览器里面是不支持的,react前端项目在启动的时候,会生成localhost:8081,这个需要在浏览器进行访问,所以所有的sdk都可以放弃了。

只能使用http api的方式用axios来进行请求,百度、阿里的都可以。但是百度的这个http api还有个不太好的地方,无法直接用localhost:8081来调用他们的api,会报错:from origin ‘http://localhost:8081’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.。但是阿里的不会,所以我们选用阿里的即可

如果真的想用百度的,可以启动一个解除web安装策略的浏览器窗口,就不会报这种跨域错误了,命令如下:

cd C:\Program Files\Google\Chrome\Application>
chrome.exe --user-data-dir="C://Chrome dev session" --disable-web-security

下面给下阿里的通义接口

const token: string = keys.qianwen_key;
const headers: { [key: string]: string } = {
    "Content-Type": "application/json", "Authorization": `Bearer ${token}`
}
export async function tongyiRequest(systemPrompt: string, userPrompt: string) {
    try {
        console.info("Calling tongyiRequest qwen-plus")
        const response = await axios.post(keys.qianwen, {
            model: "qwen-plus",
            messages: [
                { role: "system", content: systemPrompt },
                { role: "user", content: userPrompt },
            ],
        }, { headers });
        console.info("tongyiRequest response = ", response)
        return response.data.choices[0].message.content;
    } catch (error) {
        console.error("Error in groqRequest:", error);
        return null; // or handle error differently
    }
}

3、openai的tts接口,就是文本转语音接口。作者测试了腾讯、百度、阿里、微软、讯飞等平台的接口,发现百度的最简单使用,其他的都不满足使用,这里直接给出百度的代码

attention:直接用也会报跨域错误,要用前面第2点提到的解决办法

const AK = "**"
const SK = "**"

/**
 * 使用 AK,SK 生成鉴权签名(Access Token)
 * @return string 鉴权签名信息(Access Token)
 */
function getAccessToken() {

    let options = {
        'method': 'POST',
        'url': 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' + AK + '&client_secret=' + SK,
    }
    return new Promise((resolve, reject) => {
        axios(options)
            .then(res => {
                resolve(res.data.access_token)
            })
            .catch(error => {
                reject(error)
            })
    })
}

export async function BaiduHttpApiTextToSpeech(text: string) {
    console.log("BaiduHttpApiTextToSpeechtext = ", text)
    const token = getAccessToken()
    console.log("BaiduHttpApiTextToSpeech token = ", token)
    
    const response = await axios.post("https://tsn.baidu.com/text2audio", {
                'tex': text,
                'tok': await getAccessToken(),
                'cuid': 'XB1oEeNOJVjS5oYGQYlbdrWDwPcIJbCP',
                'ctp': '1',
                'lan': 'zh',
                'spd': '5',
                'pit': '5',
                'vol': '5',
                'per': '1',
                'aue': '3'
            }, {
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Accept': '*/*'
            },
            responseType: 'arraybuffer'  // This will handle the binary data correctly 这将正确处理二进制数据
        });

    console.log("BaiduHttpApiTextToSpeech response=", response)
    // Decode the audio data asynchronously
    const audioBuffer = await audioContext.decodeAudioData(response.data);

    // Create an audio source
    const source = audioContext.createBufferSource();
    source.buffer = audioBuffer;
    source.connect(audioContext.destination);
    source.start();  // Play the audio immediately
}

六、启动

npm start启动后,就可以测试上面的功能了

基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程 - 但风偏偏,雨渐渐 - 博客园

mikel阅读(685)

来源: 基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程 – 但风偏偏,雨渐渐 – 博客园

    本次演示部署环境:Windows 10专业版,转载请说明出处

下载安装Docker

  Docker官网:https://www.docker.com/

自定义Docker安装路径

Docker默认安装在C盘,大小大概2.9G,做这行最忌讳的就是安装软件全装C盘,所以我调整了下安装路径。

新建安装目录:E:\MySoftware\Docker并将Docker安装包放在目录内,这里有个小细节,安装包名称一定要改下,官网下载下来的名称叫:Docker Desktop Installer.exe,一定要修改一下,不能用这个名字,否则等下在CMD命令安装的时候就会报错说被资源占用,因为Docker在安装时会解压一个一模一样名称的exe程序,重名就会导致安装失败,所以一定要改下名字。

  在文件路径输入cmd回车

输入:


.\"Docker.exe" install --installation-dir=E:\MySoftware\Docker
语法:.\”安装程序名称” install --installation-dir=指定Docker安装的路径

   安装完成后会提示Installation sueceeded

  桌面会出现Docker图标

  启动Docker这里很多人会报这个错,这个是因为电脑没有WSL导致无法启动Docker容器。

下载WSL

  进入微软官网按步骤执行即解决,几分钟完成。

https://learn.microsoft.com/zh-cn/windows/wsl/install-manual#step-4—download-the-linux-kernel-update-package

  用管理员身份打开Powershell窗口,粘贴微软官网的命令执行下载即可

粘贴执行:

dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

 

wsl --set-default-version 2

  下载完后会启动一个这个页面,可以关闭。

  注:完成内核更新包安装后需重启系统(微软官方指南),再启动下Docker,出现这个界面就代表WSL和Docker都启动成功了。

Docker镜像存储迁移

这时进入Docker设置中,将Docker的镜像资源存放路径改一下,不然都会下载都C盘。

Docker镜像源加速配置

至关重要的地方来了,打开Docker的设置中的–>Docker Engine,然后把国内的镜像源复制进去保存,我这里提供一些,如果失效了就百度找新的。

复制代码
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://noohub.ru",
"https://huecker.io",
"https://dockerhub.timeweb.cloud",
"https://0c105db5188026850f80c001def654a0.mirror.swr.myhuaweicloud.com",
"https://5tqw56kt.mirror.aliyuncs.com",
"https://docker.1panel.live",
"http://mirrors.ustc.edu.cn/",
"http://mirror.azure.cn/",
"https://hub.rat.dev/",
"https://docker.ckyl.me/",
"https://docker.chenby.cn",
"https://docker.hpcloud.cloud",
"https://docker.m.daocloud.io"
]
}
复制代码

保存镜像源后就可以试一下拉取镜像,如果拉取不成功也可以重启下Docker,还是不行提示超时就说明镜像源失效了,就在网上搜索下新的镜像源。

测试拉取镜像

在cmd命令窗口输入:

docker pull hello-world

出现这个Status: Dowloaded newer image for hello-world:latest就代表镜像源没有问题。

安装Dify

下载Dify代码包

进入github下载Dify代码包:https://github.com/langgenius/dify

解压代码包后,把压缩后的文件夹复制到自己想要安装的目录下,这里复制一下.env.example文件,然后重命名一下改成.env

  在当前文件路径下输入CMD回车

拉取Dify依赖镜像资源

  粘贴以下命令回车,会自动下载一些依赖资源。如果你的下载失败就是镜像源失效了,换一个镜像源,重新拉取镜像。

docker-compose up -d

下载完成

  回到Docker可以都看到已经下载好的镜像全部都显示了,并且都在运行。

进入Dify后台

输入http://127.0.0.1/会自动打开Dify的页面,有人会遇到这个Internal Server Error报错,这是因为镜像下载来后,有部分镜像还在启动中或未启动,这时候将所有镜像重启一次才可以。

  重启所有镜像

创建管理员用户

  重新进入Dify管理后台,首次进入需创建管理员用户。

创建管理员用户后,将进入登录界面。

登录成功

添加AI模型

点击右上角头像-设置

成员这里可以创建企业内成员进行登录使用。

选择模型供应商

这里我本地已经安装部署好了Ollama和Deepseek R1和BGE-M3模型,如果没有部署好的请看我这篇文章本地电脑部署DeepSeek 大模型AI

  由于我本地已经安装好了Ollama,所以就找到Ollama,点击安装插件,其他供应商选择对应安装。下载可能稍慢,请耐心等待。

添加模型时,若不知模型名称,可在CMD中输入Ollama list查看本地模型名称并复制。

模型类型:

推理模型 → LLM
嵌入模型 → Text Embedding

模型名称就把刚刚复制下来的粘贴上去就可以了。

这里IP地址要注意了,由于我没有用Docker容器里部署Ollama,而是在本地电脑安装的Ollama,这里对IP就要进行特殊处理下了,需要改为:http://host.docker.internal:11434。

网络通信原理:
host.docker.internal为Docker内置域名解析,实现容器与宿主机服务的桥接。简单说就是Docker自己和我们电脑主机网络做了一个映射关系。

模型添加完成

创建应用

其实我也不太会使用,就简单粗糙的做个示范吧,要深入研究需要找下资料学习下。

这里我选择创建聊天助手(每个不同应用的作用不一样,选择与自己相符的就行)

添加一些提示词、变量、知识库、模型,设置好后在右边可以调试预览效果,调试完成后就可以发布应用了。

这里提一句,由于我自己的电脑资源很一般,所以每次一提问的时候资源就占比很高,不过等AI思考完毕后资源占用会下降。

测试结果,虽然回答是错误的。

知识库测试

我这里测试了下知识库检索,上传了6个本地文档。

然后我简单的定义了提示词后,对模型提出问题:结合知识库帮我找出住在向阳里桃源弄的人员的电话和姓名。

 

然后真的回答对了,全体起立!

这是源文件里的内容。

WSL资源控制

由于我是针对个人学习,在学习完后我发现我的电脑内存占比一直居高不下,在任务管理器查到了是一个Vmmem的进程占用,大概也知道应该是虚拟机类的占用。

搜索了下网上资源了解到vmmem是一个由WSL(Windows Subsystem for Linux)创建的虚拟进程,主要用于管理WSL2的内存和CPU资源。当WSL2运行时,Vmmem进程会占用大量的内存和CPU资源,以确保虚拟机的最佳性能。然而,这可能会导致主机系统的其他应用程序运行缓慢或无法正常运行‌。

关闭WSL服务

所以如果不用的时候可以关闭掉WSL服务。

在cmd里输入:

wsl --shutdown

关闭后电脑资源就回到正常状态了。

启动WSL服务

那如果我们后再用的时候就重新启动WSL服务就可以。

在cmd输入:

wsl

最后的最后

关于Dify的作用文中提到的只是冰山一角,它真正的厉害之处是它的工作流,由于博主知识有限,只能教大家部署应用,具体的功能开发使用还要大家自行学习,后续博主也会去学习Dify的相关知识,有机会的话就再开一贴。如有讲的不对的地方,敬请指正。

附上Dify的官方操作手册地址:https://docs.dify.ai/zh-hans

这是我整个学习过程中遇到的问题,最后结合百度和AI最后都完成解决了。

总结几个小坑:

1、WSL2的安装。

2、Docker容器镜像源的设置。

3、Dify依赖镜像的拉取。

4、Dify添加模型时IP映射设置。

觉得不错的麻烦大家动动发财的小手点下推荐,谢谢!!!