linux网络IO无论是select poll epoll都属于NIO。他们本身不负责读写数据，它们只是负责同志哪些fd已经准备就绪。BIO的核心是一个线程管理一个socket，NIO的核心是一个线程管理多个socket。虽然是non-blocking IO。但其实底层都是要进行同步阻塞的。BIO的核心是线程等待IO，NIO的核心是线程等待事件。select、poll 和 epoll 本质上都是 Linux/Unix 提供的系统调用（system call）接口，而在 C 语言中表现为函数名称。Go语言中BIO的底层实现通常是Epoll，java的BIO底层实现就不是。G语言更像把netty藏进了语言运行时，对上层暴露的接口仍然是BIO风格的。select最简单的方式获取网络中传输过来的数据是通过select，不停的轮询是否有数据到来，但是这样子就会不停的询问内核，导致用户态和内核态不停切换造成开销。一个简单的优化方式是，将自己需要监听的文件描述符通过bitmap直接告诉内核。sockfd = socket(AF_INET, SOCK_STREAM, 0); memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(2000); addr.sin_addr.s_addr = INADDR_ANY; bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)); listen(sockfd, 5); for (i = 0; i < 5; i++) { memset(&client, 0, sizeof(client)); addrlen = sizeof(client); fds[i] = accept(sockfd, (struct sockaddr *)&client, &addrlen); if (fds[i] > max) max = fds[i]; } while (1) { FD_ZERO(&rset); for (i = 0; i < 5; i++) { FD_SET(fds[i], &rset); } puts("round again"); select(max + 1, &rset, NULL, NULL, NULL); for (i = 0; i < 5; i++) { if (FD_ISSET(fds[i], &rset)) { memset(buffer, 0, MAXBUF); read(fds[i], buffer, MAXBUF); puts(buffer); } } }我们调用核提供的select方法，内核会接受一个1024个bit的bitmap（rset），不需要监听的被设置为0。我们让内核来判断是否有数据到来，这个select函数是一个阻塞函数。直到有数据才会继续执行。当有数据来的时候，内核会将有数据的FD置位，select函数会返回。这里的fd+1是最大需要的文件描述符编号。比如我最大需要的FD是9，那么内核会将0-9这10个文件描述符拿出来。当有数据的时候，可能存在同时多个FD都有数据。因此下面还是要遍历每个fd是否有数据。用户程序需要完整遍历一次bitmap才知道哪些FD是有数据的。这里提高效率主要的一点是把判断放到了内核态。但是这里的缺点是bitmap最大是1024，此外rset会被内核修改，每次都要重新开一个新的rset进行设置。这里用户态和内核态还是要频繁的拷贝这个bitmap，还是有一定开销。而且每次要重新O（n）去遍历bitmap。select是19世纪的API，因此有很多的缺陷。现在很多系统还在使用select。POLLstruct pollfd { int fd; short events; //在意的事件 short revents; //对event的回馈 }; for (i = 0; i < 5; i++) { memset(&client, 0, sizeof(client)); addrlen = sizeof(client); pollfds[i].fd = accept(sockfd, (struct sockaddr *)&client, &addrlen); pollfds[i].events = POLLIN; } sleep(1); while (1) { puts("round again"); poll(pollfds, 5, 50000); for (i = 0; i < 5; i++) { if (pollfds[i].revents & POLLIN) { pollfds[i].revents = 0; memset(buffer, 0, MAXBUF); read(pollfds[i].fd, buffer, MAXBUF); puts(buffer); } } }poll函数的函数参数少了很多。这里pollfds就是pollfd结构体的数组。5是元素的个数，50000是超时时间。这里工作方式和select类似，同样是将5个数据拷贝到内核态，然后让内核阻塞。但是这里不是bitmap了，而是pollfd的数组。这里的poll依然是阻塞函数，一个或多个文件描述符有数据的时候，内核会将pollfd进行置位。这里置位的是pollfd的revent字段。然后将poll方法返回。判断的时候我们先判断哪个event是1，把event恢复为0.因此我们可以重用这个pllfdsd。但是重新遍历，还有内核态的切换问题还是没解决。EPOLLstruct epoll_event events[5]; int epfd = epoll_create(10); for (i = 0; i < 5; i++) { static struct epoll_event ev; memset(&client, 0, sizeof(client)); addrlen = sizeof(client); ev.data.fd = accept(sockfd, (struct sockaddr *)&client, &addrlen); ev.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev); } while (1) { puts("round again"); nfds = epoll_wait(epfd, events, 5, 10000); for (i = 0; i < nfds; i++) { memset(buffer, 0, MAXBUF); read(events[i].data.fd, buffer, MAXBUF); puts(buffer); } }epfd是用epoll_create创建的参数，用于epoll_wait的时候使用epoll_ctrl是对epoll_fd进行配置，这里增加了文件描述符和事件，pollfd有fd字段和event字段，但是没有revent字段。这里循环了5次，epfd数组中存放了五个fd和对应的事件。这里和之前的select与poll不同epfd是在用户态和内核态共享的。内核还是帮我们判断哪个fd有数据到来。因此不需要用户态和内核态的开销。下面讲水平触发（LT）没有数据的时候epoll_wait也是阻塞状态的，epoll中也有置位，假设监听到一个fd有数据来了。这时候就会进行重排，将有数据的fd放在最前面的位置。然后进行返回。这里epoll_wait是有返回值的，返回有多少个fd触发了事件。比如返回3，这时候只需要遍历数组的前3个元素，对前3个元素进行处理就可以。在边缘触发ET中，ET只通知一次，所以必须一次性把数据全部读完。直到读操作返回EAGIN。redis，nginx，java中的NIO使用的都是EPOLL。水平触发（LT，默认模式）： 只要fd对应的缓冲区还有数据可读/可写，每次 epoll_wait 返回时，这个fd都会出现在就绪列表中。→ 如果不把数据读完，下次调用 epoll_wait 还会继续通知你。边缘触发（ET）： 只有状态发生变化时（比如从“无数据”变为“有数据”，或从“不可写”变为“可写”），epoll_wait 才会返回这个fd。红黑树用在 epoll 的内核管理结构里，专门用来存储和管理所有通过 epoll_ctl 添加的 fd。当你调用 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, event) 时：内核会在 epoll 实例对应的红黑树里插入一个节点，key 就是这个 fd。红黑树保证了：无重复：同一个 fd 只能添加一次。快速查找：增、删、改的时间复杂度都是 O(log n)。如果你用 EPOLL_CTL_MOD 或 EPOLL_CTL_DEL，也是通过这棵红黑树，快速定位到对应的 fd 节点，然后修改或删除。内核并不是把所有监听的fd都拿出来，把有事件的fd排到前面去。实际上，整个过程是这样的：事件发生：当一个fd上有数据到来时（比如网卡收到数据），硬件触发中断。执行回调：内核的中断处理程序会调用该设备驱动注册的回调函数。查找节点：这个回调函数会以这个fd为键，在你提到的那个红黑树中快速查找，找到对应的那个节点（这个节点里保存了你注册的事件信息和回调函数）。排入就绪队列：内核会把这个节点添加到epoll实例的一个专门的双向链表——就绪队列（rdllist）的尾部（tail）。整个过程就是一个简单的链表尾部追加操作。哪个fd的事件先处理好，它的节点就先被加到链表里，自然就排在前面，没有额外的排序算法。当 epoll_wait 返回时，它会把就绪队列里的节点从内核态复制到你传入的 events 数组中。这个复制过程，也是从就绪队列的头部（head）开始，按链表顺序依次取出的。所以，你在 events[0]、events[1]、events[2] 里看到的fd，其实就是内核就绪队列里的自然顺序——谁先准备好，谁就在前面。AIO早期的Linux AIO底层还是依赖epoll来完成的，本质上是用多路复用模拟出来的异步。第二层：为什么“早期的网络AIO”做不到这一点？Linux内核在很早期（2.6版本）就提供了libaio（POSIX AIO的Linux实现），但它只对磁盘文件（O_DIRECT）支持较好，对网络Socket的支持极差。原因有两点：Socket的复杂性：网络数据是流式的、分包的，不像磁盘块那样固定，内核很难在用户不参与的情况下完美管理TCP流的边界和缓存。内核实现偷懒：早期Linux内核的AIO实现，对于Socket，并没有实现真正的内核态异步回调机制。它只是在用户态封装了一个线程池，或者在内核里用epoll去监听Socket，等epoll告诉它有数据了，内核再帮你调用recv把数据读出来。可以说io_uring是linux正统切新一代的异步I/O标准，但它并非第一个Linux AIO实现。Linux 内核很早就有一套名为 libaio（或传统 Linux AIO）的异步 I/O 接口，但它因为设计上的诸多限制，一直没能成为通用、高效的解决方案，甚至连 Linus Torvalds 本人都曾严厉批评过它“是一个糟糕的临时的设计”特性对比传统 Linux AIO (libaio)io_uring设计哲学设计受限，并非真正的通用异步 I/O设计之初就为真正的异步，提交即返回，永不阻塞使用场景几乎只支持 O_DIRECT 模式的存储文件，无法使用页缓存，不支持网络 I/O统一支持：支持缓存文件、直接 I/O、网络 sockets（如 send, recv），甚至 openat, accept 等系统调用性能与开销每次提交和获取完成事件都需要系统调用，有上下文切换开销通过共享内存环形队列（SQ/CQ）通信，可实现零系统调用进行提交和收割，支持批量处理扩展性扩展性差，功能基本固化革命性的灵活和可扩展，甚至被视为可以“重新实现所有系统调用”的基础框架Io_uringio_uring设计时就是linux特有的 异步I/O接口。用它编写的程序，其核心部分是无法直接移植到windows平台上运行的。在epoll模式下，每次项检查事件或者发起读写都要通过epoll_wait，read，write等系统调用陷入内核，这是开销的主要来源。io_uring在工作开始之前，就在内核和用户程序之间创建了俩个共享的环形缓冲区（ring buffer）提交队列（SQ，Submission Queue）：你把要做的I/O操作请求（比如读文件、发网络包）放进这个队列。完成队列（CQ，Completion Queue）：内核完成你的请求后，把结果放进这个队列。这意味着，在理想情况下，你提交任务和获取结果，都可以通过读写共享内存完成，完全不需要调用任何系统调用。只有在需要等待新完成事件时，才可能需要调用一次io_uring_enter，或者干脆开启SQPOLL模式，让内核线程主动轮询，从而将系统调用次数降到极低。epoll只是告诉你“数据准备好了”，你还是得自己去调用read把数据搬走，这个“搬”的过程是同步阻塞的。而io_uring从你发起请求到数据就绪，整个过程都是异步的。你提交一个读请求（IORING_OP_RECV或IORING_OP_READ）后，内核会全权负责等待数据和拷贝数据，完成后将结果放到CQ里通知你，你的线程可以去干别的事。更重要的是，它还支持批量处理。你可以一次提交几十上百个请求，内核也会批量处理并将完成事件放回，这能极大地减少上下文切换次数，让CPU利用率大幅提升。等于是我告诉内核应该进行什么操作，而不是内核将数据拷贝给我，我自己做这个操作。java相关的实现java中的selector在linux上底层就对应了epoll操作。Selector selector = Selector.open();底层可能变成epoll_create(...)这里把所有的channel注册到一个selector。当这个selector查询到有事件发生的时候，Java的netty中，4.1.x之前，需要netty-incubator-transport-io_uring，4.2版本已经内置了io_uring。但是如果想使用io_uring的特性，仍然需要进行特定编码，比如判断io_uring是否可用，然后进行对应的操作。Selector 只负责事件通知（event notification），Channel 才负责实际的数据传输（read/write）。远端机器 ↓ TCP协议栈 ↓ 操作系统内核Socket缓冲区 ↓ SocketChannel.read() ↓ ByteBuffer在netty中bossgroup负责监听ServerSocketChannel。把新连接交给woker。一个链接在被建立之后永远只属于一个worker。Boss线程的任务在TCP连接建立后就结束了。不会负责后续IO。当TCP accept成功后，就会把channel注册给workerGroup。每个worker都有自己的Selector TaskQueue 线程当注册完成后，后续的读写都贵worker。阻塞非阻塞：线程状态同步异步：结果获取的方式阻塞非阻塞是当发起一个io请求时，线程是否需要停下来等待。没有数据的时候线程被挂起，就是阻塞。非阻塞就是每次都看一下有没有数据，有就读取，没有就返回不会进行等待。同步异步关注的是IO完成后，谁来通知结果。同步阻塞：自己发起，自己等待，自己获取结果。同步非阻塞：while(true){ recv() }一直问。异步阻塞：异步阻塞并没有意义，异步要达成的效果就是可以继续干其他的事情，但是阻塞有导致当前线程停住，有的优势也没了。异步非阻塞：真正的异步IO，执行后立刻返回，等待内核来回调函数。
agent学习笔记https://xiaolinnote.com/ai/agent/ageng是什么？agent本质是是一个能自主完成目标的AI系统，和传统AI核心区别在于自主性和能行动。普通语言模型在被训练完毕之后知识就被冻结。本质上就是一个文字输出器。 并且每次都会忘记之前做过什么。感知->规划->行动->再感知。Anthropic 在 2024 年底提出的 MCP（Model Context Protocol，模型上下文协议）然后是谷歌在2025年四月退出的agent2agent，解决了agent和agent之间的写作问题。agent的组成部分LLM+工具系统+记忆系统+规划记忆这里记忆系统分长短期记忆。短期记忆是当前这轮上下文中的对话内容，是在context window里的。长期记忆使用向量数据库来实现，把重要的信息embeddig之后存起来，下次用语义检索的时候可以拿回来。短期记忆的挑战是上下文窗口的限制，每次工具有大量文本，上下文很快就满了。这时候要恶魔摘要压缩（把前面的步骤浓缩成关键信息）要么滑动窗口，只保留最近的几步详细内容。但不管哪种方式都会丢失信息，怎么在记住够多和不撑爆上下文中间做取舍，是记忆刚才里最核心的设计问题。长期记忆的挑战在什么该存，什么不该存。存的太多会产生噪音，存的太少又失去长期上下文的意义。比如三个月前客服处理的问题现在通常是不重要的。常用的指数衰减公式：规划规划决定了agent能否应对复杂任务。比如任务是克隆一个写竞品分析报告，它就需要搜索资料，整理相关数据，对比分析，撰写报告。最基础的是（chain of thought）COT，思维连，核心思想是让模型把思考过程写出来，而不是直接输出最终答案。为什么这么简单一句话就能提升效果？因为 LLM 的 token 生成是逐步进行的，每一步推理的输出会成为下一步推理的输入，把中间步骤写出来，等于给了模型更多的「思考空间」。在此基础上还有Tree of thoughts（ToT）思维数。它不是走一条线性的推理链，而是在每个推理节点上展开多个可能的分支，然后评估每个分支的质量，选出最优的路径继续往下走。一种是先规划后执行，也就是plan and execute模式，先让LLM输出一个完整的步骤列表ReAct模式，每走一步就工具当前结果重新思考下一步该做什么。不提前指定计划。好处是灵活性高。总的来说agent的节奏很简单：规划 决策 执行 结果存入记忆 再决策。workflow agent Tools分别是什么？Toolstools是整个体系里最简单，最底层的概念，本质是是一个按定制格式暴露给LLM的函数：普通函数是给程序员调用的，Tool是给LLM调用的。所以要配一份LLM才能看得懂的schema（名字、描述、参数类型）工具本身没有决策能力，它甚至不知道自己应该在什么时候被使用。工具的设计要职责单一，描述精确，错误信息要清晰。Anthropic 在2024年提出的MCP，就是把工具的注册，描述调用做成一套标准的协议。AgentTools就是拿着工具的角色。Agent的运行方式是Thought、Action、Observation反复循环的模式。WorkFlow假设要做一个客服系统，大致流程就是先判断用户问的是什么问题。再去知识库里检索相关内容 生成一个回答。这些步骤分支完全可以在代码里写死。Workflow 最大的优点是可预测、可控、好调试。你在代码里看到什么，它就做什么，不会有任何「惊喜」。生产环境里出了问题，你可以打断点逐步追，精确定位是哪个节点出了故障。这种确定性在线上系统里非常珍贵。Anthropic 在他们的 Agent 工程实践中总结了几种常见的 Workflow 编排模式，值得了解一下。• 第一种叫「Prompt Chaining」（提示链），就是把一个大任务拆成多个小步骤，前一步的输出作为后一步的输入，像流水线一样串起来。• 第二种叫「Routing」（路由），先用一个 LLM 做分类判断，然后根据分类结果把请求分发到不同的处理分支，前面客服系统的例子就是典型的路由模式。• 第三种叫「Parallelization」（并行化），把可以同时进行的子任务并行执行，最后汇总结果，这在需要多维度分析的场景下特别有用，比如同时从多个数据源检索信息。• 第四种叫「Orchestrator-Workers」（编排者-工人），一个中央编排者负责分配任务，多个 Worker 各自完成子任务，适合任务可以分解但子任务之间相互独立的场景。另一个非常实用的模式是evaluator-optimizer 评估者优化者模型。一个 LLM 负责生成输出，另一个 LLM（或者同一个模型换一个角色）负责评估这个输出的质量，如果评估不通过就把反馈给回生成者，让它改进后重新输出，如此循环直到评估通过或者达到最大重试次数。Agent是让llm自己决定下一步该干什么，WorkFlow是把流程写死，每一步怎么走都是确定的。Agent的推理模式推理模式，本质是是从直接输出答案变成一步一步推导答案。就像在数学题写的时候把过程写出来了。COT就是在prompt里加一句，让我们一步一步思考，LLM就会把推理的过程写出来。COT有俩种方式第一种叫 Zero-shot CoT，做法非常简单，直接在 prompt 末尾加上「让我们一步步思考」这句话就行了，LLM 会自己展开推理过程，不需要你提供任何示例。这种方式的优势是零成本、即插即用，缺点是 LLM 的推理格式和深度完全靠它自己发挥，不太稳定，有时候会写得很详细，有时候又跳步。• 第二种叫 Few-shot CoT，你需要在 prompt 里给几个带有完整推理过程的例子，让 LLM 照着这个格式来模仿。比如你先写一道数学题，把每一步怎么算、最后得出什么结论都写清楚，LLM 看到这个模板之后就会按照同样的格式展开推理。Few-shot 的效果更稳定，特别适合输出格式要求比较固定的场景，代价是需要你提前准备高质量的示例，而且示例本身也会占用 token。ReactReAct是Reasoning and Acting的缩写，在思维连里面插入真实的行动。ReAct的效果比纯CoT或者纯Act-only效果来的好。纯CoT推理的再好也拿不到真实数据。纯Act-only走的是另一个极端，让LLM直接输出工具调用序列，不写任何思考过程。ReAct 的精妙之处就在于把推理和行动交织在一起：Thought 帮助 LLM 分析当前局势、决定下一步该做什么，Action 让它把决策落地为真实操作，Observation 把外部世界的反馈带回来，三者互补形成闭环。推理为行动提供方向，行动为推理提供事实，这才是 ReAct 比前两种方案都好用的根本原因。假设你问 Agent「2024 年苹果公司和谷歌的市值谁更高？差多少？」，如果只靠 CoT，LLM 只能说出它训练时知道的数字，可能已经不准了。但用 ReAct，整个过程会是这样的：Thought: 这道题需要两家公司的实时市值数据，我得先查苹果的市值 Action: search Action Input: 苹果公司 2024 年市值 Observation: 苹果公司 2024 年市值约为 3.5 万亿美元 Thought: 好，苹果的数字有了，再查谷歌的 Action: search Action Input: 谷歌 2024 年市值 Observation: 谷歌 2024 年市值约为 2.1 万亿美元 Thought: 两个数字都有了，苹果 3.5 万亿，谷歌 2.1 万亿，苹果更高，差距是 1.4 万亿 Final Answer: 苹果公司 2024 年市值约 3.5 万亿美元，谷歌约 2.1 万亿美元，苹果更高，差距约 1.4 万亿美元Plan-and-execute你可以把 ReAct 和 Plan-and-Execute 的区别，类比成「边走边问路」和「先看地图再出发」的区别。reflection在ReAct、Plan-and-Execute额外加了一层自我检查自我修正的环节。replan用来解决定死计划中途遇到的意外该怎么处理。动态 Replan 的做法是在每个步骤执行完之后，把当前结果和剩余计划一起交给规划模块，让它判断「原来的计划还合理吗，需不需要调整」。如果需要，就生成一份新的剩余步骤计划，替换掉原来的。这样既保留了 Plan-and-Execute「先规划再执行」的结构优势，又不会因为计划太僵硬而在意外情况下翻车。代价是每步都多了一次「重新评估计划」的 LLM 调用，token 消耗会增加。任务拆分任务拆分是因为一次性处理太复杂的任务很容易出错，把大人物拆成小任务，每步聚焦一件事情准确率就会明显提升。分俩种，一种是静态拆分，一种是动态拆分。静态拆分就是设计任务流程，规定乘一个确定的workflow，每一步干什么按什么顺序执行，全部写死。比如写技术博客，固定拆成手机资料，整理大纲，撰写，润色校对。好处是行为可预测，出问题方便排查。坏处是灵活性低，遇到没设计到的情况就容易卡主。动态拆分是把任务拆解这件事情本身也交给LLM来做。也就是plan-and-execute核心思想。任务的粒度把控很重要。太细导致LLM调用次数变多，总token消耗上升。 事情负责的太多也会导致出错的概率上升。更好的做法是，不要在开始的时候就把所有步骤的粒度定死，而是根据每一步实际难度调整。核心逻辑就是先尝试完成任务，做的好就继续做，作品不啊哈哦就拆分。先试 不好就拆。好的任务拆分应该满足三个条件完备性：所有步骤加在一起能不能覆盖原始需求。独立性：每个步骤的职责边界是不是清晰，职责有没有重叠可验证性：每个步骤执行完了之后能否用简单的标注判断是不是作对了。开源框架mem0是最火的agent记忆框架之一，核心思路是把记忆做成服务层，可以使用memory.add()存记忆、memory.search()查记忆。底层的embedding 去重冲突消除全部都做好了。 mem0特别适合个性化记忆的场景，比如记住每个用户的偏好和习惯，按不同user_id做记忆隔离，不同用户记忆互不干扰。并且它同时支持向量存储和图（知识图谱）存储。此外知名的还有letta，前身是大名鼎鼎的MemGPT，还有一个值得关注的是ZAP，引入了时间感知的概念。记忆模块配合记忆和agent的写作模式为读、用、写。读：用户发来一个心情求，agent先去翻档案：得到用户过往决策，以及用户画像。用：用户每一条消息、模型的每一次输出、工具调用返回的每一个结果都追加到Message。每次调用LLM都带上完整离散写：任务完成把本次任务产生的新知识写回持久化存储。反思烦死机制就是让agent在完成一个步骤后，自我评估输出质量判断有没有问题，相当于人写完东西自己再看一遍。代价是多一次调用，所以一般只在关键点使用，而不是每次都做。步骤级烦死是在每一步就进行反思，任务级烦死是整个任务执行完一次做一次整体评估。多agent还可以互相评论反思。可以设置一个单独的agent来审查执行agent。多agent多agent分为三类第一类是流水线模式，Agent 之间按固定顺序依次执行，前一个完成后交给下一个，像工厂的装配线；第二类是层级模式，有一个 Orchestrator（指挥者）负责分配任务、收集结果，其他 Agent 各自执行分配到的子任务；第三类是协商模式，多个 Agent 之间没有严格的上下级关系，通过互相沟通、辩论来达成一致。多agent写作分为共享状态和局部状态。首先是状态结构要分层，通常会把状态分成「全局状态」和「局部状态」两层。全局状态存放所有 Agent 都需要读取的信息，比如用户的原始请求、当前任务进展、最终输出，这些是共享的。局部状态存放每个 Agent 自己的中间结果，比如搜索 Agent 找到的候选文档列表、代码 Agent 生成的草稿代码，这些在 Agent 内部使用，不会直接暴露给其他 Agent，避免信息污染。其次是写入规则要明确。最简单也最可靠的做法是「只追加不覆盖」，每个 Agent 完成工作后把结果追加到状态里，而不是修改已有的字段。LangGraph 的 State 更新机制就是这个思路，你定义好 State 的 schema，每个节点返回的是一个「增量更新」，框架帮你合并到全局状态里，这样就不会出现互相覆盖的问题。最后是错误状态的处理。如果某个 Agent 执行失败了，它的错误信息也应该写入状态，而不是悄悄吞掉。后续的 Agent 或者 Orchestrator 读到这个错误状态后，才能做出正确的决策，比如跳过这一步、换一个 Agent 重试、或者直接终止任务。
时间APITSCTSC（Time Stamp Counter，时间戳计数器）是现代 CPU 中一个非常底层的硬件计数器，用来记录自 CPU 上电以来经过的时钟周期数。TSC 本质上是一个 64 位寄存器，通常位于 CPU 内部，每当 CPU 时钟周期推进一次，它的值就 +1（在现代 CPU 中已经不完全等价于“每个核心周期”，但概念上仍是“高精度单调计数器”）。在 x86 架构中，可以通过指令读取：RDTSC（Read Time Stamp Counter）TSC 常用于性能分析，因为它的分辨率非常高（纳秒级甚至更细）。操作系统、数据库（比如 MySQL InnoDB）、高性能网络系统都会用 TSC 做：函数耗时统计lock contention 分析cache miss 成本分析TSC ≠ wall clock timevDSO优化vDSI是linux提供的一种机制，把一部分内核提供的只读系统调用映射到用户态的空间里，让用户态直接调用，而不需要陷入内核。vDSO = “内核提前帮你准备好的用户态函数库（但数据由内核更新）”典型函数包括：gettimeofdayclock_gettimetime传统路径：用户态函数调用 → syscall 指令 → CPU 切换到内核态 → 内核处理 → 返回用户态问题在于：syscall 有 上下文切换成本需要保存/恢复寄存器可能污染 CPU cache / pipeline高频时间查询会非常昂贵比如：一个高性能服务每秒可能调用 clock_gettime 上千万次具体方式：内核在进程地址空间映射一段“只读共享代码”用户态直接调用这些函数不发生 syscall数据仍由内核更新（比如时间）Linux CLOCK_MONOTONICCLOCK_MONOTONIC是Linux 提供的一种单调递增时钟，用于测量时间间隔，而不是表示当前真实时间。他的关键特性是：时间只会向前走不会因为系统时间被修改而改变。类型含义是否受系统时间影响CLOCK_REALTIME真实世界时间（Unix time）会（NTP、手动修改会跳变）CLOCK_MONOTONIC单调时间（从某个起点开始计时）不会CLOCK_MONOTONIC 的起点是：系统启动（boot）之后的某个未定义时间点注意：不等于 Unix epoch（1970）不可预测只保证单调递增他不能跨机器比较。也不能转换成unix时间戳。它不是时间而是时间差工具。
那些底层使用了Rocksdb的项目元数据使用了RocksDB的框架kafka：的streams默认将持久化状态的中间计算结果Pulsar的底层存储，也就是Bookkeeper：的默认存储实现中，Rocksdb用来维护Ledger元数据索引rocketmq：RocketMQ 在 broker 端通过 RocksDB 来保存 消费偏移量、订阅组管理/配置、认证授权数据等元数据Ceph：使用Rocksdb来管理对象映射和磁盘分配记录。Doris在后端BE存储中使用了rocksdb保存元数据的键值对。主要状态数据使用RocksDBMySQL/MariaDB中存在innodb的另一个存储引擎选择，MyRocks，底层使用了rocksdb作为存储引擎。顺序批量写入性能更高，但是查询延迟稍高。Kvrocks，pikiwidb，Tendis他们都兼容Redis协议，并且底层使用rocksdb作为主要的持久化协议存储。tidb：底层使用的tikv的内部使用了rocksdb作为存储实现。Flink：Checkpoint可以选择，通过使用rocksdb存储到本机。ClickHouse 提供了一个叫 EmbeddedRocksDB 的表引擎，可以让用户把一个表的数据存放在一个 RocksDB 实例中，并通过 ClickHouse SQL 访问它。Nacos等这些底层依赖sofa-jraft的框架底层都采用rocksdb作为存储，不过也允许切换为其他存储实现。MongoDB提供了rocksdb存储引擎插件，优化高写入场景
LLM的有趣技术https://www.bilibili.com/video/BV1gEcmzzE4P/语言模型的进化多模态大语言模型是如何将所有的感官，对齐到语言系统中的？这本质上属于多模态对齐（multimodal alignment）与表征学习（representation learning）的交叉问题。可以从结构层面+训练机制+对齐目标三个维度来精确理解。人类的感官数据在原始形态上完全是异构的，所以核心问题变成，如何把连续的信号映射到一恶搞可以被语言模型理解的符号空间。现代多模态大模型（如 GPT-4o、Gemini、Claude 等）通常采用如下结构：不同模态编码器 → 统一 embedding 空间 → LLM（语言核心）每种感官都有专属编码器，比如图像的Vit，音频的wav2vec，视频的时空tranformer，文本的token embedding。这些编码器的目标都是抓成一个高维语义向量。那么如何对其到共享语义空间？对比学习对比学习：Contrastive learning 代表作为CLIP。训练目标：图像embedding=描述该图像的文本。即最大化：正样本（匹配图文）的相似度负样本（不匹配）的距离结果图像和语言落在同一个语义空间中跨模态生成Generative Alignment图像 → 文本（captioning）音频 → 文本（ASR）视频 → 文本（描述）训练方式输入：图像 embedding目标：生成对应文本 token让 LLM 学会“用语言解释感官”联合训练直接训练一个模型处理：[图像 tokens] + [文本 tokens] → 预测下一个 token把非语言模态“token化”（tokenization of modalities）语义一致性最终目标不是“数据形式一致”，而是：不同模态表达相同“意义”时，其内部表示接近一张猫的图片“a cat sitting on a sofa”在 embedding 空间中应接近为什么语言成为“中心模态”？语言有几个决定性优势可以表达复杂结构：时间因果抽象概念标注成本最低：图像/音频的语义通常通过语言标注获得LLM 的核心能力：chain-of-thoughtplanningabstraction都发生在语言空间。语言不是被对齐的对象，而是“语义坐标系”所有感官数据都被投影到这个坐标系中。换句话说：图像 ≠ 变成文字图像 → 映射到“可以用语言解释的语义空间”思维连chain-of+thought。本质是让模型在给出最终答案之前，显式或隐式的生成中间推理步骤，从而提升复杂任务的正确率。传统语言模型直接输出答案，COT是在输出和最终答案中增加了思考步骤。模型被引导像人一样一步步想。可以将一个复杂的问题，拆成多个简单的子问题。在训练或推理是。没有CoT只有最终loss，有CoT：每一步都有结构化的模式。本质上相当于在token级别上引入推理轨迹约束。CoT 提示过程是一种最近开发的提示方法，它鼓励大语言模型解释其推理过程。思维链的主要思想是通过向大语言模型展示一些少量的 exapmles，在样例中解释推理过程，大语言模型在回答提示时也会显示推理过程。这种推理的解释往往会引导出更准确的结果。零样本思维链（Zero Shot Chain of Thought，Zero-shot-CoT）提示过程是对 CoT prompting 的后续研究，引入了一种非常简单的零样本提示。他们发现，通过在问题的结尾附加“Let's think step by step”这几个词，大语言模型能够生成一个回答问题的思维链。从这个思维链中，他们能够提取更准确的答案。外部工具ReAct 和 Toolformer 是两种典型的“让模型使用工具/外部能力”的方法论，但它们的设计哲学和实现路径差异很大ReActReAct = 推理（Reasoning）+ 行动（Acting）交替进行模型在推理过程中，不只是“想”，还会：显式生成思考链（Thought）决定调用工具（Action）接收工具返回结果（Observation）再继续推理本质：把推理过程外显，并嵌入工具调用显式推理链（Chain-of-Thought）类似 CoT，但更进一步推理过程中可以“打断去查资料”动态工具调用工具使用是 runtime 决策不需要提前训练工具调用能力强交互性适合：多步推理（multi-hop QA）agent系统复杂决策任务ToolformerToolformer = 让模型自己学会什么时候调用工具和 ReAct 最大区别：ReAct：靠 prompt 引导Toolformer：靠训练学出来Toolformer 做了三件关键事：自监督生成工具调用数据给模型少量 seed examples，然后：模型自动：在文本中插入 API 调用（如 calculator, search）比较调用前后 loss如果调用让预测更好 → 保留自动构造训练数据！训练模型用这些数据 finetune，让模型学会：何时调用工具如何调用如何利用返回结果推理时自动调用最终模型：不需要 ReAct prompt自然生成类似：The result is [Calculator(23*45)] = 1035支持的工具类型（原论文）CalculatorSearch APITranslationQA systems既然有眼镜，草稿纸，计算器等工具了。LLM很像一个活生生的人。因此可以说小龙虾的火爆是必然的。scaling law1980s网页的增长速度鳗鱼存储系统的增长速度。我们有朝一日可以存下互联网上所有的数据。所以我们有了google。因此google的出现是必然的。人类的智能不过是算力而已。象棋中，机器每多搜一层胜率就会提高一点。每一条曲线都是一个颜色固定一个算力。随着算力的增加，就需要越来越多的数据来喂饱它。纵轴是next token的lossopenai在训练150b模型的时候，没人知道openai是否能成功，当时openai只是做了小规模的验证。一切都是历史的必然：Transformer在长上下文的能力上比LSTM的能力要好很多。所以Transformer活下来了。但是没有人知道是否存在一个模型能够用更少的参数训练出更号好模型的结构。The bitter lesson中提到心智的实际内容是极其复杂且无法简化的；我们应当停止试图寻找思考心智内容的简单方式，例如思考空间、物体、多重主体或对称性的简单方法。所有这些都是任意的、本质上复杂的外部世界的一部分。它们不应该是内建（预设）的，因为它们的复杂性是无穷无尽的；相反，我们应该只内建那些能够发现并捕捉这种任意复杂性的元方法。使用AI大模型是Transformer，prompt 工程实际上是attention工程（这就是为什么GPT刚出来的时候会有非常长的提示词。）没有合适的提示词LLM会倾向于车轱辘话（一部分是因为instruct fine-tuning/ RLHF）提示词应该分解问题。也就是要构建一个合适的抽象。如何让大语言模型更好的工作，在如今就是如何让大语言模型更接近人类的工作方式。
Epoch-Based Reclamation算法https://www.zhihu.com/question/406428296/answer/1345620324概述EBR是在无锁或低锁并发结构中常用的聂村回收机制。核心目标是解决并发环境中内存安全释放的问题。在并发数据结构中，删除节点不等于可以立即释放内存。比如在删除的时候，其他线程可能仍然持有指针，但删除线程无法知道。常见的类似方案RCU：read-copy-update。写操作不直接修改正在被读取的对象，而是创建新的副本。类似COW引用计数，开销稍大。Hazard Pointers：精确但复杂EBR的思想是：延迟释放核心Bw-Tree（B-link Tree without latches）是一种无锁的并发B+树变体。目标是实现高并发环境下实现高吞吐的索引结构。避免传统B+树锁竞争的问题。Bw-Tree主要针对内存数据库和无锁结构。每个操作在开始的时候注册到一个Epoch，可以根据物理时间，每10ms作为一个epoch操作开始时增加Epoch counter，操作结束时减少Epoch counter操作过程中，产生的垃圾都放到epoch内部的garbage list上当这个epoch的所有操作都结束了，即counter为0时，这个epoch的所有资源都可以被释放用上图举例，这是BwTree的内存垃圾回收。众所周知BwTree是一个latch-free的高性能索引，既然要latch-free那么内存分配一定不能拖后腿了。在对索引进行操作时，例如删除索引的一条数据，并不能直接释放内存，否则无锁算法很容易出现ABA问题。所以在操作过程中产生的垃圾，都放到epoch的garbage list上，当所有线程都退出这个epoch就可以安全地释放掉所有资源。（事实上，上面这个图还是Centralized，Open-BwTree里用了扩展性更好的Decentrailzed实现）EBR算法的回收是机会式回收。是许多lock-free数据结构的默认实现方式。相比RC相比RC（reference counting）引用计数法，EBR有更好的缓存局部性。引用计数每次操作都要原子写入。这会导致cpu的缓存行竞争严重。多次出发缓存行失效是严重的性能问题。EBR 也不是完美的。一个线程的sllep会导致全局epoch无法推荐。导致重试列表无限增长。内存回收也可能会不及时导致内存占用更高。特性Reference CountingEpoch Reclamation读开销高（atomic inc）极低写开销atomic dec低cache contention严重很小回收方式即时延迟内存占用低可能较高线程停顿影响无有扩展性差好EBR 的效率来自：消除 per-access 原子操作，把回收同步从“每次访问”降低到“偶尔 epoch 推进”。
MQTT学习笔记简介写给那些已经学习过消息队列的人（）或者间接连接。比如移动网络，卫星网络等。协议非常轻量header只有2字节。MQTT只有一种通信模型Publisher → Broker → Subscriber例如temperature/sensor1 temperature/sensor2设备只关心topic，topic是层级字符串，支持通配符订阅。如temperature/+ temperature/#不同于消息队列mqtt有统一的开源标准化协议。由OASIS 维护。通信模式MQTT支持多种模式如点对点：每个消息只消费一次Producer → Queue → Consumer发布订阅：每个订阅者都收到消息。流处理MQTT的Qos从高到低依次为Qos 0：至多依次（at most once1）发布者发送消息后不等确认，直接发送下一条消息。代理收到消息后直接转发给订阅者，同样不等待订阅者确认。Qos 1：至少一次（at least once）这是中等级别，确保消息至少被送达一次，但可能重复。发布者发送消息后，会等待代理返回一个 PUBACK（发布确认）包。如果超时未收到确认，发布者会重新发送消息，直到收到确认为止。代理收到消息后，会存储消息，并确保将其发送给订阅者（同样需要订阅者的确认）。消息不会丢失知道重试成功。Qos 2：恰好一次（exaclly once）最高的级别。确保消息仅有一次被送达。这个机制通过四次握手实现（four way handshake）PUBLISH：发布者发送消息。PUBREC：代理收到后回复“已接收”。PUBREL：发布者收到确认后，发送“释放”信号。PUBCOMP：代理回复“完成”，此时双方才确认消息处理完毕。Qos机制只是传输层的机制，只负责消息物理上从生产者到消费者，但无法感知消费者应用层代码是否执行成功。PUBREC/PUBCOMP 由 MQTT client library 自动发送。而不是等你业务处理完成后再发。MQTT没有类似消息队列中的DLQ（死信队列）的实现。MQTT语义是传递保证，而不是处理保证。因此broker不会感知业务是否处理成功。QoS2保证的是“协议层 exactly-once”，不是“业务 exactly-once”。这里的ack和kafka不同的是收到消息就ack，而不是完成处理才ack。MQTT 本身只是一个消息传输协议，是否把消息保存到磁盘、保存多久、是否清理，取决于具体的 MQTT Broker 实现和配置。不同 Broker（如 Eclipse Mosquitto、EMQX、HiveMQ）策略会不同。可以从几个机制来理解。如果要保证消费成功可以通过应用层ackProducer → MQTT → Consumer ↓ 处理成功 ↓ publish ack message例如topic: order/create topic: order/create/ack和消息队列的区别mqtt 出现的更早，是1999年的，kafka是2011年。并且一般kafka的生产者都是内网的机器有单独的稳定的IP。而mqtt生产者可能只有NAT的IP，并且无法固定。以EMQX距离，吞吐量能来到50w到100w。kafka在100w-1000w。由于许多应用层消费逻辑都只适配了kafka的。你甚至可以把MQTT消息桥接到kafkaIoT Device / Client │ ▼ MQTT Broker (EMQX / HiveMQ / Mosquitto) │ │ MQTT → Kafka Bridge / Connector ▼ Kafka Cluster │ ├── Stream processing (Kafka Streams / Flink / Spark) ├── Storage (Data lake / DB) └── MicroservicesMQTT为极低带宽+不稳定网络设计。最初为IOT、传感器网络设计。适用于设别算力弱。网络延迟高的场景。MQTT协议甚至不强制要求持久化。直接进行实时转发。MQTT的设计重点通常不是吞吐，而是轻量级通信。如EMQX默认所有数据都存储在内存中。通过规则引擎（Rule Engine）和数据桥接（Data Bridge），可以将消息、客户端上下线事件等数据持久化到外部数据库（如 MySQL、PostgreSQL、Redis、MongoDB 等）。这种方式通常用于业务数据归档或与外部系统集成。总的来说MQTT是无状态的。MQTT的顺序消费还要结合Qos来保证。无论是Qos1还是2都不保证严格顺序。这个顺序更像是TCP的顺序，业务层要自己控制执行顺序（毕竟ack是自动回复的，而不是业务层实现的）。
虚拟线程和协程平台线程在Java25中对虚拟线程做了非常多的优化。那么到底虚拟线程解决了什么问题呢？哪些场景下能用到呢？和GO语言的go routine有什么区别呢？我们从小学二年级就学过，在JDK21之前Java中只有平台线程。所谓平台线程就是将线程所有的调度权交给操作系统进行处理。操作系统会有时间片轮转，多级队列等机制来决定当前让哪个线程执行，这让写程序的人觉得操作系统的线程资源几乎是无限的。问题就出在了这里，操作系统的调度是有代价的，操作系统内核直接调度，导致线程切换都需要进入内核态，这涉及了完整的上下文的切换，系统需要保存和恢复寄存器，恢复上下文，进行调度决策，这部分的开销较大。当我们创建的线程数远大于实际上操作系统逻辑核心数时，操作系统为了这些线程更公平的调度就需要频繁的挂起和恢复线程，确保每个任务都能向前推进。线代的操作系统使用的都是抢占式调度，操作系统可以通过中断的方式抢占当前的CPU并进行上下文切换’这种切换是强制的称为抢占式多任务处理模式。我们这里以一个tomcat服务器为例，如果CPU有10个核心，那么这台机器如果只能同时处理10个http请求，这显然是不能接受的。但我们发现，这个http请求其实大部分的时间，并不是在使用CPU做计算，这个HTTP请求在处理过程中可能需要调用第三方的服务接口，或者是查询数据库，redis缓存等操作。在执行这些操作的时候，我们自然希望让操作系统能够挂起这些线程切换到其他线程来执行。当然同时处理过多的线程还是会出现上面提到的CPU资源被抢光，因此tomcat设置了一个默认值，也就是200作为线程池的大小。也就是说最多同时调度处理200个请求。当然具体的参数应该按照负载的类型和CPU的型号进行修改，比如CPU如果配置好，或者当前服务只是单纯的一个请求转发服务的话我们就可以将这个参数调的更高。是不是听起来非常的熟悉，没错，这就是我们经常背的八股文IO密集型和CPU密集型线程池的资源设置逻辑。虚拟线程-协作式调度所谓IO密集型的潜在含义是，这个服务不太用CPU，经常可以把CPU出让给其他线程使用。如果线程池中线程数设置的太多了，容易导致操作系统频繁的挂起程序导致性能下降，但设置的太少了也会导致在线程数被打满的情况下，处理器依旧没有满负荷运行导致性能浪费。那有没有什么方式可以解决这个问题呢？有的兄弟有的，那就是Java的虚拟线程。虚拟线程采用的解决方式十分的简单。既然操作系统的调度那么浪费时间，我把这个调度放到用户态不就好了？JVM可以自己调度线程。在Java中对虚拟线程采用的调度方式叫做协作式调度。协作式调度是当一个程序出现挂起或者阻塞，比如sleep函数时，那么当前的CPU资源就会默认被分配给另一个需要执行的线程。在这段java代码中，程序中创建了比当系统可支配调度的线程数多一个的线程。这种情况下，多的那一个线程仍然可以被运行。比如运行如下代码时，程序能够成功的输出这里的New virtual threadimport java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) throws InterruptedException { int busyThreads = Runtime.getRuntime().availableProcessors(); int newThreadIndex = 0; List<Thread> threads = new ArrayList<>(); // 创建并启动"死循环虚拟线程" for (int i = 0; i < busyThreads; i++) { int id = i; Thread virtualThread = Thread.ofVirtual() .name("busy-virtual-thread-" + id) .start(() -> { System.out.println("Busy virtual thread " + id + " started"); while (true) { try { Thread.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); threads.add(virtualThread); } // 等待一会儿，让死循环线程占满平台线程 Thread.sleep(1000); // 创建并启动一个新虚拟线程尝试执行 Thread newVirtualThread = Thread.ofVirtual() .name("new-virtual-thread-" + newThreadIndex) .start(() -> { System.out.println("New virtual thread " + newThreadIndex + " started"); }); threads.add(newVirtualThread); // 等待一会儿查看结果 Thread.sleep(10000); } }因为其他线程的循环中的sleep函数将会使其他虚拟线程将当前执行权让给这个新来的线程。但是如果我们稍微改一下代码把Sleep删掉，重新运行这段代码import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) throws InterruptedException { int busyThreads = Runtime.getRuntime().availableProcessors(); int newThreadIndex = 0; List<Thread> threads = new ArrayList<>(); // 创建并启动"死循环虚拟线程" for (int i = 0; i < busyThreads; i++) { int id = i; Thread virtualThread = Thread.ofVirtual() .name("busy-virtual-thread-" + id) .start(() -> { System.out.println("Busy virtual thread " + id + " started"); while (true) { // 纯 CPU 循环，不阻塞 } }); threads.add(virtualThread); } // 等待一会儿，让死循环线程占满平台线程 Thread.sleep(1000); // 创建并启动一个新虚拟线程尝试执行 Thread newVirtualThread = Thread.ofVirtual() .name("new-virtual-thread-" + newThreadIndex) .start(() -> { System.out.println("New virtual thread " + newThreadIndex + " started"); }); threads.add(newVirtualThread); // 等待一会儿查看结果 Thread.sleep(10000); } }我们会发现new virtual thread这段永远不会输出。无论是在JDK21还是25都是如此。这就是协作式调度，相互之间需要商量什么时候能把资源给其他线程。这种调度模式的，好处就是不需要让jvm定期中断操作，因为这会产生一定的性能开销。坏处就是如果你有11个完全不休息的任务也就是11个线程，要运行在一个10线程的CPU上，其中一个任务就会持续无法被调度运行。go混合调度go中的虚拟线程我们通常叫做go routine ，它选择了不一样的套路。我们都知道go的线程调度模型是基于GMP模型。相同的，GO也是把调度带到了用户态程序中。我们通常把Goroutine称作协程，主要这里的协就是协作的协。早期的go选择的是和现在java相似的纯协作式调度也就是说，在go1.13及之前如果你运行如下代码package main import ( "fmt" "runtime" "time" ) func main() { // 获取 CPU 核心数 numCPU := runtime.NumCPU() fmt.Println("Number of CPUs:", numCPU) busyGoroutines := numCPU + 1 // 创建比 CPU 多的 goroutine fmt.Println("Starting", busyGoroutines, "busy goroutines") // 启动大量 CPU-bound goroutine for i := 0; i < busyGoroutines; i++ { id := i go func() { fmt.Printf("Busy goroutine %d started\n", id) for { // 纯 CPU 循环 } }() } // 等待一秒，让 busy goroutine 占满 CPU time.Sleep(time.Second) // 再启动一个新 goroutine newGoroutineIndex := 0 go func() { fmt.Printf("New goroutine %d started\n", newGoroutineIndex) time.Sleep(10 * time.Second) }() fmt.Println("Submitted new goroutine, waiting 5 seconds...") time.Sleep(5 * time.Second) fmt.Println("Main function exits") }最终的会和java一样，无法输出New Goroutine，甚至会直接卡死。但是在1.14及以后版本，运行相同的代码时，New Goroutine又能够成功的输出了。go语言，很神奇吧。这是因为在后续的版本中Go语言采用了协作式加抢占式调度。如果一个goroutine在同一个逻辑处理器上连续运行10毫秒，就会被标记为可抢占。也就是说，在go中高版本有俩种可能性导致协程切换，一个是协程阻塞或者挂起，另一个是运行太久会被其他协程抢占。总结一下，我们不难得出虚拟线程的主要好处是创建和销毁线程的开销非常小，而且都是用户态的操作。因此大多数情况下，不需要和平台线程一样对虚拟线程进行池化。因此我们也不需要担心具体的线程池参数配置，这在以往，通常要根据业务属性和压测结果来进行设置。虚拟线程的切换开销也是用户态的操作，相对于系统级别的陷入才能进行的线程切换，虚拟线程在频繁需要切换线程的情况下性能会更好。在知道了这些原理后如果你觉得线程调度太麻烦，想程序员每次执行的代码都占用一个线程，所有异步操作都默认在一个线程上调度那么你就实现了python的，GIL锁。如果你想通过异步事件驱动来避免操作系统的频繁上下文切换，那你就实现了springcloud getway中的webflux。
有趣的开源项目记录分类整理1. 开发与编程工具Go-blueprint - 命令行交互式创建和初始化Go项目，支持网页端选择Pake - 用Rust开发的工具，可将网页打包成macOS、Windows、Linux支持的桌面客户端应用，封装体积比Electron小20倍manim - 3Blue1Brown作者开发的强大数学动画引擎，专为数学和科学可视化设计，推荐使用社区版manimCEDeepface - 轻量级Python人脸识别工具，支持年龄、性别、情绪、种族识别，准确率超过人类RealtimeSTT - 开源实时语音转文本库，采用流式处理技术，延迟极低，支持模型量化Amphion - 开源音频、音乐和语音生成工具包，支持TTS、SVC、TTA、VC等功能Skyvern - 基于视觉理解的网页自动化工具，摆脱传统DOM依赖，采用多智能体协作架构index-tts2 - 文本转语音工具2. 数据库与数据处理Teable - 现代化的无代码数据库与数据协作平台，基于PostgreSQL构建，类Airtable界面DiceDB - 开源高性能内存数据库，作为Redis替代品，适合高速缓存和实时系统Docling - 文档解析工具，支持导出markdown和JSON，支持OCRGitdiagram - 将GitHub仓库转换为交互式架构图3. 系统工具与操作系统PowerToys - Windows系统增强工具，包含快速启动器、图片批量处理、拾色器、文本提取器等Ventoy - 多系统启动U盘制作工具Windows via Docker - 从Docker安装Windows，在Linux中通过浏览器访问，支持Win7-11WebVM - 在网页端运行的Linux虚拟机，甚至可以运行可执行文件Jellyfinweb - 媒体服务器Web界面4. 网络与文件共享LocalSend - 本地网络安全文件共享工具，无需互联网连接FilePizza - 基于WebRTC的点对点文件传输，文件直接从发送者浏览器传到接收者浏览器File-transfer-go - Go语言实现的文件传输工具Netbird - 点对点VPN/内网穿透工具AList - 聚合网盘工具Linux Mirrors - Linux镜像源管理工具5. 监控与管理Infisical - 开源密钥管理工具，类似HashiCorp Vault，支持微服务架构Beszel - 轻量级服务器监控平台，支持Docker容器监控，资源占用低XPipe - 终端连接工具Openlist - 开源列表管理工具6. 安全与渗透测试Hacking tool - 一体化黑客工具箱，包含各类漏洞扫描和攻击工具Sherlock - 社交媒体账号搜索工具，支持400多个网站查询7. 人工智能与分布式计算EXO - 分布式AI计算集群框架，允许连接日常设备形成AI计算集群Pansou - 开源搜索引擎Weepwiki - 知识库/wiki系统项目详细记录开发与编程工具Go-blueprint通过命令行交互式的创建和初始化Go的项目。也可以在网页里直接选择。Pake一个用Rust开发的小工具，可以把网页打包成macOS、Windows、Linux三种系统支持的桌面客户端应用。Pake的封装比Electron的封装小20倍。manim3Blue1Brown的作者开发的强大动画引擎。专为数学和可视化科学设计。能生成高质量的数学动画。推荐使用manim和manimCE（社区版），因为会更稳定。DeepfaceDeepface是一个轻量级的Python人脸识别工具。支持年龄、性别、情绪、种族识别。它是混合面部识别框架，集成了多种识别模型。准确率已经超过了人类。RealtimeSTT是一个开源的实时语音转文本（Speech-to-Text，STT）库，专为低延迟应用设计。该项目由KoljaB开发，采用Python语言编写，能够将语音数据实时转换为文本，适用于语音助手、实时字幕生成、会议记录等多种场景。采用流式处理技术，在语音输入的同时实时输出转写结果，延迟极低。基础模型（tiny）运行时内存占用约400MB，大型模型（large-v2）在GPU环境下显存占用约4.2GB。通过INT8量化技术，模型内存占用可减少75%。AmphionAmphion是一个由上海人工智能实验室、香港中文大学（深圳）数据科学学院和深圳市大数据研究院联合开发的开源音频、音乐和语音生成工具包。该项目于2023年12月开源，旨在支持可重复的研究，帮助初级研究人员和工程师快速进入音频生成领域。它支持TTS、SVC、TTA、VC等功能。SkyvernSkyvern的最大突破在于摆脱了传统自动化工具对DOM结构和XPath选择器的依赖。传统工具如Selenium、Playwright需要为每个网站编写定制脚本，一旦页面布局发生变化，脚本就会失效。而Skyvern采用"视觉理解"模式：通过分析网页截图，结合视觉语言模型来理解页面内容和元素功能，像人类一样"看懂"网页并自主决策操作步骤。采用多智能体协作架构，包含规划器、执行器、验证器、视觉语言模型四个核心模块。index-tts2文本转语音工具。数据库与数据处理Teable开源项目Teable是一个现代化的无代码（no-code）数据库与数据协作平台，类似于Airtable，但基于PostgreSQL构建并且是开源的。它的目标是让技术和非技术用户都能方便地创建、管理和协作复杂的数据应用，而不必编写代码。Teable提供了一个类电子表格界面，用户可以像使用Excel或Airtable那样直接操作数据，但底层则是真正的PostgreSQL数据库，因此具备高性能、强一致性和可扩展性。DiceDBDiceDB是一个开源的高性能内存数据库（in-memory database），其设计目标是为现代实时应用和缓存场景提供极低延迟、高吞吐量的数据存储与访问能力。它最初由DiceDB团队开发，并于2024年底开源，采用BSD 3-Clause许可。DiceDB将数据全部保存在内存中（非持久化），因此适合用作高速缓存层或对性能敏感的实时系统后台。相比传统磁盘数据库，它能够显著降低读写延迟。DiceDB设计初衷之一是作为Redis的替代品，兼容部分Redis协议/命令。Docling文档解析工具，支持导出markdown和JSON格式，支持OCR功能。Gitdiagram将GitHub仓库转换为交互式的架构图。只需要输入项目地址就可以生成架构图。系统工具与操作系统PowerToys增强Windows 10和Windows 11操作系统。包含了快速启动器（快速搜索启动应用程序）、批量图片大小调整工具、拾色器、屏幕取色器、文本提取器等。Ventoy多系统启动U盘制作工具。Windows via Docker从Docker安装Windows然后从Linux中通过浏览器访问，支持Windows 7-11，默认安装Windows 11。WebVM在网页端运行的Linux虚拟机，甚至可以运行可执行文件。JellyfinwebJellyfin媒体服务器的Web界面。网络与文件共享LocalSend允许在本地网络上安全的和附近设备共享文件和消息。无需互联网连接。FilePizza基于WebRTC实现，省去了文件上传功能。文件会直接从发送者的浏览器直接发送到下载者的浏览器上。数据从未保存到中间的服务器中。传输速度更快。分享文件也支持多人下载。但是下载的时候要保证浏览器打开。File-transfer-goGo语言实现的文件传输工具。Netbird点对点VPN/内网穿透工具。AList聚合网盘的工具。Linux MirrorsLinux镜像源管理工具。监控与管理Infisical开源密钥管理工具，类似HashiCorp Vault。集中管理存储敏感信息，提供强大的安全措施来管理数据。特点包括集中管理和安全性。项目和环境分离。API和CLI支持。以微服务为例，可以给每个微服务创建一个文件夹结构。清晰的组织和管理每个微服务的密钥。BeszelBeszel是一款开源的轻量级服务器监控平台，专为简化服务器和容器监控而设计。它支持Docker容器监控、历史数据记录、智能告警等功能，拥有友好的Web界面和极低的资源占用，非常适合个人开发者和小型团队使用。轻量级设计：Beszel采用Go语言开发，资源占用极低，在1核512MB内存的主机上也能流畅运行，相比Prometheus+Grafana等传统监控方案更加轻量。Docker深度支持：能够实时监控每个容器的CPU、内存、网络使用情况，并记录历史数据，帮助用户快速定位性能瓶颈。XPipe终端连接工具。Openlist开源列表管理工具。安全与渗透测试Hacking tool一个一体化黑客工具箱。融合了各类漏洞扫描和攻击工具。包括DDos、XSS等。Sherlock社交媒体账号搜索工具，输入用户名可以查询400多个网站的社交账号。项目没有提供UI，可以通过命令行来使用。但是没有除了gitee的国内站点。人工智能与分布式计算EXO其核心理念是"在家中使用日常设备运行自己的AI集群"。该项目允许用户将家中的iPhone、iPad、Android手机、Mac、Linux电脑等日常设备连接起来，形成一个强大的分布式AI计算集群，无需昂贵的NVIDIA GPU即可运行大型AI模型。自动模型分割框架。系统会根据当前网络拓扑和各设备的可用资源，智能地将模型分割成多个部分并分配给不同设备处理。例如，两台各有8GB内存的设备可以共同运行需要16GB内存的模型，突破了单设备的内存限制。EXO实现了零配置的自动设备发现功能，无需手动配置IP地址或网络设置。只要设备连接到同一网络，系统就能自动识别并纳入计算集群，极大简化了部署流程。EXO原生支持RDMA over Thunderbolt 5技术，在支持该接口的设备之间可实现高达99%的通信延迟降低。Pansou开源搜索引擎。Weepwiki知识库/wiki系统。新增项目Follow支持在一个地方关注所有喜欢的内容，比如网站博客，社交媒体账户。是一个一体化的信息中心。支持Windows、macOS、Linux。
JEP 491虚拟线程和synchronizedJava24解决了虚拟线程同步使用问题 - 极道调度方式调度方式分为协作式调度和抢占式调度协作式调度：线程不让出执行权会导致其他线程饿死。抢占式调度：操作系统或运行时强制中断正在运行的线程，将 CPU 分配给其他线程。是否切断线程由调度器决定。即使你的 Java 代码没有调用 yield()，操作系统仍然可以在任何时刻抢占线程。在java中虚拟线程就是协作式调度，平台线程就是抢占式调度。Java 虚拟线程（Virtual Thread）只有在发生“可挂起（park）”的点时，才会与当前平台线程（carrier / platform thread）解绑，平台线程才有机会去执行其他虚拟线程。如阻塞，或者线程礼让。import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; public class Main { public static void main(String[] args) throws InterruptedException { // 使用虚拟线程创建一个固定数量的 carrier threads ExecutorService executor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual().factory()); //平台线程小于50 int busyThreads = 50; int newThreadIndex = 0; // 提交“死循环虚拟线程” for (int i = 0; i < busyThreads; i++) { int id = i; executor.submit(() -> { System.out.println("Busy virtual thread " + id + " started"); while (true) { // 纯 CPU 循环，不阻塞 } }); } // 等待一会儿，让死循环线程占满平台线程 Thread.sleep(1000); // 提交一个新虚拟线程尝试执行 executor.submit(() -> { System.out.println("New virtual thread " + newThreadIndex + " started"); }); // 注意：你可能看不到 New virtual thread 的打印 // 因为所有平台线程都被占满 Thread.sleep(10000); } } 上述发生的原因是java的协程是协作式调度的go1.13之前是协作式调度为主，1.14之后支持抢占式调度。现在 Go 采用的是混合调度策略，兼具两种调度的优势：运行时向 M（OS线程）发送信号实现强制抢占，防止 goroutine 长时间占用 CPUpackage main import ( "fmt" "time" ) func main() { busyThreads := 50 newThreadIndex := 0 // 启动"死循环"goroutine for i := 0; i < busyThreads; i++ { id := i go func() { fmt.Printf("Busy goroutine %d started\n", id) // 纯 CPU 循环，不阻塞 for { } }() } // 等待一会儿，让死循环 goroutine 开始执行 time.Sleep(2 * time.Second) // 提交新 goroutine go func() { fmt.Printf("New goroutine %d started\n", newThreadIndex) }() // 等待 10 秒 time.Sleep(10 * time.Second) } 因此go就不会出现这样的问题。但在1.13中就会有问题。java21和25都存在这个问题。背景简单来说就是synchronized阻塞的时候，无法切换到其他协程，导致当前平台线程被阻塞，不能运行其他虚拟线程。理论上可以使用ReentrantLock来解决虚拟线程是Java 21通过JEP 444引入的轻量级线程，由JDK而非操作系统提供。虚拟线程显著减少了开发、维护和观察高吞吐量并发应用程序的工作量，使应用程序能够使用大量线程。线程必须被_调度_才能做有用的工作，即被分配到处理器核心上执行。对于平台线程，JDK依赖于操作系统的调度器。对于虚拟线程，JDK有自己的调度器，它将虚拟线程分配给平台线程，然后由操作系统像往常一样调度。虚拟线程在执行阻塞操作（如I/O）时会卸载，当阻塞操作准备完成时，操作将虚拟线程重新提交给JDK的调度器，调度器将虚拟线程重新安装到平台线程上以继续运行代码。为何虚拟线程被固定到平台线程在Java中，虚拟线程被固定（pinned）到平台线程的原因主要与synchronized关键字的实现机制有关。虚拟线程在synchronized方法内部运行代码时无法卸载。考虑以下synchronized从套接字读取字节的方法：synchronized byte getData() { byte buf = ...; int nread = socket.getInputStream().read(buf); // Can block here ... }期望：如果该read方法没有实际字节二阻塞，我们希望正在运行虚拟线程getData从其载体上写着。这将释放一个平台线程，以便 JDK 的调度程序可以在其上安装不同的虚拟线程。实际：不幸的是由于getData是synchronized，JVM将正在运行的虚拟线程固定getData到其载体上。固定可防止虚拟线程卸载。因此，该read方法不仅会阻塞虚拟线程，还会阻塞其载体，从而阻塞底层操作系统线程，直到有字节供读取原因java中的synchronized关键字是基于监视器实现的，每个对象都与一个监视器关联，该监视器可以被获取（锁定）、持有一段时间，然后释放（解锁）。一次只能有一个线程持有对象的监视器。在JVM中，哪个线程持有对象紧绳器的信息是基于平台线程（即操作系统线程）来跟踪的，而不是基于虚拟线程。当虚拟线程执行synchronized实例方法并获取与实例关联的监视器时，JVM是虚拟线程的载体平台线程持有监视器，而不是虚拟线程本身。如果虚拟线程在synchronized方法中卸载，JDK的调度器可能会将另一个虚拟线程安装到刚刚释放的平台线程上。由于这个新虚拟线程的载体，JVM会认为它持有与实例关联的监视器，这将破坏互斥性，因此JVM积极组织虚拟现场在synchronized方法中卸载。如果虚拟线程在执行synchronized方法时调用了Object.wait(), 它会在JVM中阻塞知道被Object.notify()唤醒，并且载体重新获取监视器。由于它在synchronized方法中执行，以及它的载体在JVM中阻塞，所以虚拟线程也被固定危害频繁的固定（尤其是长时间固定）可能会损害可扩展性，导致饥饿甚至死锁，因为没有虚拟线程可以运行，因为所有平台线程要么被虚拟线程固定，要么在JVM中被阻塞。JEP 491旨在改变JVM对synchronized关键字的实现，允许虚拟线程在synchronized方法或语句中获取、持有和释放监视器，独立于它们的载体。这将允许虚拟线程在被阻塞时卸载，释放其载体平台线程给JDK调度器，从而提高应用程序的可扩展性通过允许在synchronized方法和语句中阻塞的虚拟线程释放其底层平台线程，供其他虚拟线程使用，从而提高Java代码在使用synchronized时的可扩展性。解决了虚拟线程在synchronized方法中不能卸载的问题，这个问题限制了能够用于处理应用程序工作负载的虚拟线程数量。同时，改进了诊断工具，以识别虚拟线程未能释放平台线程的情况，通过JDK Flight Recorder (JFR)记录jdk.VirtualThreadPinned事件来识别代码中的问题区域。由于synchronized关键字不再固定虚拟线程，因此不再需要jdk.tracePinnedThreads系统属性，该属性会在虚拟线程在synchronized方法中阻塞时打印堆栈跟踪，但可能会引起性能问题。解决了开发者在选择synchronized和java.util.concurrent.locks包中的API时的困惑，现在可以根据实际需要选择，而不必因为虚拟线程固定问题而被迫使用ReentrantLock。JEP 491提议重新实现synchronized关键字，允许虚拟线程获取、持有和释放监视器，而不需要将这些操作与它们的载体线程绑定，解决了虚拟线程与监视器交互的问题。本提案涉及对Object.wait()和Object.notify()机制的更改，允许虚拟线程在等待和重新获取监视器时挂起和恢复，而不会锁定其载体平台线程。本提案识别了一些虚拟线程仍然会固定的情况，例如在类初始化器内阻塞、等待其他线程初始化类以及在类加载期间解析符号引用时阻塞。