新闻详情

新闻详情

首页 / 资讯中心 / 详情

孪生神经网络点选识别实践:从数据构造到阈值校准

发布时间:2026/9/28 16:09:51来源:尧图网络
孪生神经网络点选识别实践:从数据构造到阈值校准
简介一套基于Python孪生神经网络的点选验证码识别项目面向深度学习入门者也适合作为毕设、课程设计、大作业、工程实训或初期项目立项的参考方案帮助理解图像相似度匹配和验证码点选识别思路。项目以相同字符构造两张图样本由孪生网络判别候选区域是否为指定文字在4090上训练100轮后测试集准确率可达98.6%以上基本覆盖常见点选类验证码场景。资源共13个文件压缩包约67.23MB以Python脚本为主包含vgg16.py、siamese.py、train.py、predict.py等7个py文件另有config.py环境配置、requirement.txt依赖清单、README说明、示例图片及zip格式数据集数据放入data目录并采用id_序号命名便于替换图片后重新训练。整体流程覆盖数据准备、模型训练到预测调用已有149人学习下载代码定位为参考资料而非开箱即用成品使用时要具备一定Python和深度学习基础能够自行调试报错并按需扩展功能。1. 孪生神经网络点选识别把找目标变成比对两张图点选识别在业务里通常长这样页面给出一张背景图图上散布着若干候选对象用户要按提示词点选其中一类目标。基于python孪生神经网络实现的点选识别核心思路是训练一个共享权重的卷积网络把参照图与背景图中的每个候选块分别编码成特征向量用向量距离判断像不像最后挑最像的位置输出。这个方案不依赖目标检测的anchor和NMS也不用标注几十个类别只要把图切成对就能训练。适合刚接触度量学习、手头标注数据不多、又被传统目标检测调参折磨过的开发者。下面按数据、模型、训练、排障的顺序把从0到1落地的过程完整讲一遍。2. 孪生神经网络如何适配点选识别核心结构与选型理由2.1 孪生网络的本质共享权重与距离度量孪生神经网络Siamese Network最早被广泛用于人脸验证、签名比对这类任务输入两张图输出一个相似度分数。它并不是某个新的卷积结构而是一种训练组织方式——两个分支各自提取特征但两个分支共享同一套权重。共享的意思是参照图和候选块在推理时走的是同一个编码器参数完全一样。把点选识别拆开看一张背景图上散布着多个候选块系统还需要一个参照图告诉你目标长什么样。把参照图与背景上的每个候选块分别组成一对那么找目标就变成了在所有候选块里挑出与参照图最相似的那一块。这正是孪生网络最擅长的场景。更妙的是换目标类别时不需要重新训练分类器只需要换一张参照图模型本身不用动。共享权重带来的直接好处是参数少、不容易过拟合。两个分支用同一套卷积核提取特征保证参照图和候选块落在同一个特征空间里进行比较如果分开训练两个网络特征空间不一致距离就没有可比性。训练目标也很直观正样本对参照图 目标所在候选块在特征空间里要靠近负样本对参照图 非目标候选块要推远这个目标用对比损失Contrastive Loss来优化。2.2 为什么用相似度匹配而不是直接回归坐标我见过不少刚接手这个需求的同学第一反应是用YOLO这类目标检测网络直接输出坐标。这个思路不是完全不可行但有几个很现实的痛点。第一点选场景的目标往往很小只有几十像素见方目标检测模型对小目标的召回率本身就要调很久。第二直接回归坐标是连续值回归问题训练样本量不够时很容易震荡预测出的坐标还可能在目标边缘来回跳。第三点最关键上层业务经常换目标类型今天点星形图标明天点红色圆形如果每次都重新标注、重新训练检测器迭代周期以周为单位。用孪生网络做度量学习本质上把问题转换为这一对图是不是同一个物体模型对具体类别不敏感对长什么样敏感。当发现某个候选块与参照图足够近就点它。这个策略还有个附加价值能输出相似度分数给上层做拒绝判断。点选场景经常出现参照图的特征在背景里不明显的情况模型不应该硬点应该返回一个低置信度让业务层决定是否重试。分类模型给的是类别概率虚警率高孪生网络给的是特征距离阈值语义直观得多。2.3 点选识别的任务拆解与两条建模路线把点选识别工程化需要先把抽象需求拆成可计算的子任务。输入有两样东西一张参照图A以及一张背景图B背景图上标注了若干候选块坐标。输出是一个点击坐标。路线A命中式匹配把背景图按坐标切成N个候选块C1...CN逐一组成(A, Ci)对执行前向计算得到N个距离值取距离最小且低于阈值的那一块作为输出坐标。路线B全局嵌入把整张背景图编码成特征图用相关滤波或注意力机制找到目标位置类似目标跟踪领域的SiamFC。路线B理论上更优雅但训练时对目标尺度变化敏感实现复杂度高排查问题也更难。本篇沿着路线A讲理由很直接数据容易构造、推理逻辑直白、中间每一步都可以单独验证适合从0到1落地。真实业务里背景图上的候选块数量一般不超过几十个逐个配对在算力上完全可接受。等路线A跑通了、数据攒够了再往路线B升级不迟。2.4 对比损失的形式与超参数选择假设两个嵌入向量为a和c标签y1表示同一目标y0表示不同目标。对比损失定义为L y * d² (1 - y) * max(margin - d, 0)²其中d是欧几里得距离margin是负样本对之间至少要隔开的距离。这个公式的含义是正样本对距离越近越好损失按平方增长负样本对距离只要超过margin就不再贡献损失小于margin就继续推远。margin控制区分度太小会让负样本对挤在一起推理时误点率升高太大会让训练初期损失很大收敛变慢。我一般从1.0开始然后看验证集上正负样本距离分布的直方图重不重叠重叠多就加大margin。嵌入维度也是要调的参数。128维是我常用的默认值候选块数量超过50个时我会升到256维。64维在数据量很小的情况下也能用但边界样本容易混在一起。还需要注意损失里的距离计算方式PyTorch的pairwise_distance默认是用L2范数后面推理时也要保持一致的度量方式不要训练用欧氏距离、推理时改成余弦相似度两套度量下的排序结果会不一样这点在2.4节末尾先打个预防针。3. 点选识别数据集怎么做从合成样本到训练对构造3.1 点选识别数据集的独特形态很多做图像入门的同学都是从cifar10、coco2017这类公开数据集开始接触深度学习的但点选识别数据集的形态完全不同。它不是一张图 一个类别标签而是由参照图、背景图、候选框坐标组成的三元组结构。拿到一个含数据集的项目时先别急着找文件夹里的图片先去找标注文件。一个可用的点选识别数据集通常包含三部分背景图集合参照图集合以及一个记录候选框的标注文件。标注文件里每行记录一个候选块字段包括它所属的背景图ID、类别名、像素坐标矩形以及这个类别对应的参照图文件名。注意这里和分类任务的区别类别只是用于构造正负样本对的索引模型最终学习的是特征空间中同一目标和不同目标的距离关系。数据集的目录结构我一般这么组织简单直观dataset/ ├── bg/ # 背景图命名如 bg_001.png ├── target/ # 参照图命名如 target_star.png ├── json/ │ └── annotations.json # 候选框与类别的标注 ├── train_pairs.csv # 训练样本对索引 └── val_pairs.csv # 验证样本对索引背景图和参照图可以用jpg压缩但要注意jpg在高压缩比下会产生块状伪影这会干扰特征提取。建议背景图用jpg质量80以上参照图用png无损格式。候选框坐标统一记录为左上角(x1, y1)和右下角(x2, y2)不要混用中心点格式后面切块时很容易因为格式不统一而翻车。3.2 用python脚本合成训练数据真实业务里的点选样本通常只有几千张用来训练孪生网络是不够的。常见的做法是用python脚本合成数据准备一批背景纹理图在背景上随机放置字符和图形同时输出标注。合成数据的好处是能完全控制目标的字体、颜色、旋转角度、遮挡程度还可以根据需要批量生成难例。合成脚本的核心逻辑是确定背景尺寸随机选字体和字符按随机位置把字符渲染到背景上同时把这块区域记录下来。下面这个脚本生成的是带点选字符的场景import random import json from PIL import Image, ImageDraw, ImageFont, ImageFilter BG_W, BG_H 640, 360 CHARS ABCDEFGHJKLMNPQRSTUVWXYZ23456789 BG_FILES [bg_01.png, bg_02.png] # 预先准备好纯色或纹理背景 def make_sample(bg_path, out_path, font_path, num_targets5): bg Image.open(bg_path).resize((BG_W, BG_H)).convert(RGB) draw ImageDraw.Draw(bg) font ImageFont.truetype(font_path, size48) annos [] for _ in range(num_targets): ch random.choice(CHARS) x random.randint(30, BG_W - 100) y random.randint(30, BG_H - 100) draw.text((x, y), ch, fill(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)), fontfont) annos.append({char: ch, bbox: [x, y, x 60, y 60]}) with open(out_path.replace(.png, .json), w) as f: json.dump({file: out_path, annos: annos}, f) bg.save(out_path)这段代码里有几个关键参数。num_targets控制背景上候选块的数量我建议训练数据里在3到10之间随机变化模拟不同密度。size是字号固定48是刻意的为了让参照图和候选块在后续切块时分辨率基本一致。字体路径建议一次准备三种不同风格的中文黑体、楷体、arial混合使用不然模型会过拟合到单一字体的笔画粗细。合成场景不够还需要做背景干扰。随机在背景上画几条直线、几个圆圈相当于模拟真实页面的线条和水印。这些干扰物不进入标注作为背景噪声存在能让模型被逼着去学字符本身的形状而不是这块区域颜色跟周围不一样。这一步对后面的泛化能力影响很大偷懒省掉的话模型在真实页面上会疯狂误点。3.3 把合成样本切成候选块与样本对有了背景图和标注还要把数据转成孪生网络需要的样本对。这里的关键细节是候选块需要加上边距再切不要只切字符围成的紧致框。字符渲染在图上有笔画粗细、有轻微旋转紧致框会把笔画的边角裁掉导致正样本对的输入和参照图差异很大。我一般给bbox外扩10像素再resize到统一尺寸。import csv import random from PIL import Image PAIR_SIZE 128 # 网络输入尺寸 def cut_candidate(bg_path, bbox): img Image.open(bg_path).convert(RGB) x1, y1, x2, y2 bbox x1 max(0, x1 - 10); y1 max(0, y1 - 10) x2 min(img.width, x2 10); y2 min(img.height, y2 10) return img.crop((x1, y1, x2, y2)).resize((PAIR_SIZE, PAIR_SIZE)) def build_pairs(bg_path, annos, out_csv): rows [] for i, anno in enumerate(annos): target_block cut_candidate(bg_path, anno[bbox]) # 正样本候选块 for j, other in enumerate(annos): if i j: continue neg_block cut_candidate(bg_path, other[bbox]) rows.append([bg_path, i, 0, positive]) rows.append([bg_path, i, j, negative]) with open(out_csv, w, newline) as f: writer csv.writer(f) writer.writerow([bg, target_idx, cand_idx, label]) writer.writerows(rows)PAIR_SIZE是网络输入尺寸128是比较平衡的选择。设成224是照搬ImageNet习惯但在小目标点选场景收益不大、还拖慢推理速度。rows里记录的是索引而不是图片路径训练时再读取图片这样CSV文件不会太大也方便做在线数据增强。负样本的采样策略值得单独说。代码里用的是全量负样本即每个候选块都会和其他候选块组成负样本对。候选块少时没问题候选块超过10个后负样本对数量会爆炸。生产环境我一般改成随机抽样每个正样本只配8到10个负样本并且在后续进度中逐步换成难负样本——也就是那些特征很像但类别不同的候选块。难负样本对提升判别能力的效果比单纯加数量明显得多。3.4 数据集划分别让数据泄漏毁掉验证集划分训练集和验证集时最容易踩的坑是按图片随机分而不是按目标实例分。假设合成脚本里同一个字符Q出现在训练集的背景图和验证集的背景图里模型在训练时已经见过这个字符的渲染风格验证集准确率虚高上线后遇到新字体就崩。正确的做法是拆合成批次。生成数据时给每个字符的字体、颜色、渲染参数打上批次ID划分为训练批次、验证批次、测试批次三者之间不共享字符实例。这样验证集的准确率才是真实的泛化能力。验证集还承担一个任务给模型选阈值。我在验证集上会额外输出每个样本对的距离值汇总成正负样本的距离直方图。两个分布的重叠区间越小模型越可靠重叠大说明特征区分度不够先别急着调阈值应该回头检查负样本是不是太简单这个细节放在第5章展开。4. 用PyTorch搭建孪生网络模型定义、训练循环与推理脚本4.1 共享权重的编码器设计假设你已经能跑通python基础环境conda里建好虚拟环境、pip装好torch我们就可以直接进入模型搭建。孪生网络的核心代码不复杂关键约束只有一个两个输入必须走同一个编码器不能各写一套。下面用PyTorch定义一个适合点选识别的编码器。import torch import torch.nn as nn class Encoder(nn.Module): def __init__(self, in_channels3, embedding_dim128): super().__init__() self.features nn.Sequential( nn.Conv2d(in_channels, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.fc nn.Sequential( nn.Flatten(), nn.Linear(128 * 16 * 16, 256), nn.ReLU(inplaceTrue), nn.Linear(256, embedding_dim), ) def forward(self, x): return self.fc(self.features(x)) class SiameseNetwork(nn.Module): def __init__(self, embedding_dim128): super().__init__() self.encoder Encoder(embedding_dim) def forward(self, target, candidate): emb_t self.encoder(target) # (B, 128) emb_c self.encoder(candidate) return emb_t, emb_c这段代码的要点是SiameseNetwork内部只有一个encoder成员没有为两个输入各建一个编码器。forward里两次调用同一个encoderPyTorch会自动让两次前向共享梯度这正是共享权重的实现方式。输入尺寸128×128经过三次MaxPool后变成16×16所以全连接层的输入维度是128×16×1632768。如果你想换成更大的骨干网络比如ResNet18可以直接把Encoder替换成torchvision.models.resnet18(pretrainedTrue)去掉最后一层分类头把fc接在全局平均池化后面。但注意预训练模型在合成数据上未必更快收敛因为合成图和ImageNet图像域差异很大。4.2 对比损失与训练循环对比损失的实现要小心一个细节pairwise_distance返回的是每个样本对的距离形状是(B,)。我们同时需要它参与正样本的平方项和负样本的铰链项所以直接基于它写损失函数即可。class ContrastiveLoss(nn.Module): def __init__(self, margin1.0): super().__init__() self.margin margin def forward(self, emb_t, emb_c, label): dist torch.nn.functional.pairwise_distance(emb_t, emb_c, p2) loss torch.mean( label * dist.pow(2) (1 - label) * torch.clamp(self.margin - dist, min0).pow(2) ) return loss训练循环和普通分类网络没有本质区别不同之处在于DataLoader返回的是一个输入对和一个标签。这里给一个最小可运行的训练脚本骨架from torch.utils.data import Dataset, DataLoader from PIL import Image import torch.optim as optim class PairDataset(Dataset): def __init__(self, csv_path, bg_dir, target_dir, transformNone): # 读取CSV后按索引映射到图片路径 self.pairs [] # 每项: (target_path, cand_path, label) self.transform transform def __getitem__(self, idx): t_path, c_path, label self.pairs[idx] t_img Image.open(t_path).convert(RGB) c_img Image.open(c_path).convert(RGB) if self.transform: t_img self.transform(t_img) c_img self.transform(c_img) return t_img, c_img, torch.tensor(label, dtypetorch.float32) model SiameseNetwork() criterion ContrastiveLoss(margin1.0) optimizer optim.Adam(model.parameters(), lr1e-4) for epoch in range(30): for t_img, c_img, label in DataLoader(train_ds, batch_size64, shuffleTrue): emb_t, emb_c model(t_img, c_img) loss criterion(emb_t, emb_c, label) optimizer.zero_grad() loss.backward() optimizer.step() print(fepoch {epoch} loss {loss.item():.4f})这里学习率用1e-4是孪生网络训练常见的稳妥值。用Adam不用SGD因为对比损失的梯度在margin附近不连续Adam的自适应步长能缓解这个问题。batch size建议在32到128之间太小的话正负样本比例的波动会很大损失曲线看起来会像心电图。训练过程中的监控指标不能只看loss。我习惯在每个epoch结束时抽样100个正样本对和100个负样本对分别计算平均距离。正样本平均距离要稳定下降负样本平均距离要稳定上升这个趋势比loss的绝对值更能说明模型在学。如果两者都不动说明学习率太低或者数据本身没信号如果负样本距离一直贴着margin走说明负样本对太简单一推就超过边界了。4.3 推理脚本切块、配对、排序、输出坐标训练完成之后推理脚本的逻辑与训练时完全一致这是孪生网络的一大好处——推理时只需要做前向计算不涉及非极大值抑制那些后处理纠缠。推理步骤是加载背景图读取候选框逐个切块resize成128×128与参照图组成对计算距离排序取最小值。import torch def infer_click(model, target_img, bg_img, candidates, threshold0.8): model.eval() target_tensor preprocess(target_img) # 1x3x128x128 best_dist, best_idx float(inf), -1 with torch.no_grad(): emb_t model.encoder(target_tensor) for i, bbox in enumerate(candidates): cand_block cut_candidate(bg_img, bbox) # 与训练时相同的外扩与resize cand_tensor preprocess(cand_block) emb_c model.encoder(cand_tensor) dist torch.nn.functional.pairwise_distance(emb_t, emb_c).item() if dist best_dist: best_dist, best_idx dist, i if best_dist threshold: return None, best_dist # 置信度低拒绝点击 cx (candidates[best_idx][0] candidates[best_idx][2]) // 2 cy (candidates[best_idx][1] candidates[best_idx][3]) // 2 return (cx, cy), best_dist注意preprocess必须和训练时的transform保持一致。最容易翻车的是训练时对参照图和候选块都做了随机旋转、颜色抖动等增强推理时却忘了关掉随机操作导致每次前向结果都不一样。正确做法是训练时用带随机性的transform推理时用只有归一化、没有随机性的preprocess两者分开写不要共用同一个函数。阈值threshol的设定我留到最后一章讲它直接决定业务的误点率和重试率不是拍脑袋设的。推理性能方面候选块数量在20个以内时CPU推理都能在几百毫秒内跑完候选块超过50个时建议用批量前向把50个候选块拼成一个batch一次性过编码器比逐个前向快得多代码里只需加一个torch.stack。5. 点选识别训练与推理避坑5个让准确率暴跌的常见问题5.1 训练loss很低但验证集准确率上不去现象是训练过程中对比损失降到0.01以下正样本对距离接近0负样本对距离全被推到margin之外但到了验证集上误点率仍然很高。原因是典型的过拟合。合成数据里的背景纹理、字符颜色组合是有限的模型记住了训练集里某个颜色区域就是正样本而不是学到字符形状。验证集里换了纹理和颜色立刻失效。解决办法是增加数据增强的多样性尤其是背景说增强。对背景图做随机亮度抖动、高斯模糊、叠加轻微噪声让模型没有稳定的捷径可走。另外把负样本采样改成每次随机抽取而不是每epoch用固定的负样本集合这样同一张图在不同epoch构造的样本对不同模型没法背答案。5.2 负样本太简单模型学到的是颜色块面积而不是形状现象是训练时负样本对距离很快就超过margin损失不再下降但点选时把所有颜色鲜艳的区域都当成了候选目标。我最早做这个项目时还真翻过这个车。原因是合成脚本里负样本是随机字符字符形状差异很大A和3本来就不像模型随便用个粗粒度特征就能分开。当前场景需要的区分是同一类字符但笔画略有不同这种难负样本在原数据里几乎不存在。解决方法是主动构造难负样本。把同一个字符用不同字体渲染把相近形状的字符如0和O、1和I放在同一个背景里让它们互为负样本再把每个正样本对距离排名靠前的负样本捞出来硬塞进下一轮训练。这就是hard negative mining效果立竿见影误点率通常能降一个量级。5.3 验证集划分不当导致数据泄漏准确率虚高现象是模型在验证集上的准确率高达98%但部署到新页面后立刻掉到70%。这是典型的验证集和训练集共享了目标实例。原因是按背景图随机划分训练集和验证集时同一批合成数据里的同一字符、同一种字体被拆到了两个集合中。模型见过这个字符的渲染风格在验证集上属于开卷考试。解决方法是按合成批次划分数据或者按字符的字体和渲染参数划分。让验证集里出现的字符实例在训练集中完全没出现过。别小看这一步它决定了后续所有调参判断是否可信。验证集不准后面调阈值、挑模型全是在自欺欺人。5.4 推理速度慢点选响应时间不可接受现象是候选块超过30个后单次推理耗时从几百毫秒涨到两秒以上业务方直接拒绝上线。原因是逐个候选块串行前向每次前向都有Python函数调用开销和GPU/CPU切换开销。孪生网络本身的计算量不大但碎片化的调用让开销放大了。解决方法是把候选块打包成batch并行前向。把所有候选块resize后拼成一个tensor只调用一次编码器然后一次性计算所有距离。实测30个候选块的情况下打包前后耗时能差4到5倍。另一个优化点是只对参照图做一次编码不要在循环里反复计算参照图的嵌入向量这个在很多初版代码里都有这个问题。5.5 合成数据训练出的模型在真实页面上跨域崩坏现象是合成数据上准确率不错一换到真实页面截图几乎所有候选块的距离都变得很大模型频繁拒绝点击。原因是合成数据的光照、字体渲染、背景纹理和线上真实环境差异太大图像域发生了偏移。孪生网络对域的敏感性比分类模型更明显因为它比较的是两个输入的细节差异域偏移会同时污染两个输入的特征。解决办法分两步走。第一步用线上真实页面截图做微调把真实页面切成候选块用少量人工标注 大量模型自动标注的方式把真实域的数据混入训练集。第二步在推理前对输入做归一化预处理比如统一转成灰度图或统一直方图匹配。灰度图会丢失颜色信息但如果线上的点选目标本身不依赖颜色区分转灰度反而能消除合成数据的颜色伪造痕迹提高跨域稳定性。6. 阈值校准与难例挖掘把模型接进点选业务流程模型训练完只是第一步真正让点选识别可用的是阈值校准。点选业务最怕两件事把不是目标的东西点了以及把目标漏掉。这两个错误在孪生网络里都体现为一个距离值所以阈值必须定在两个分布的均衡点上而不是拍脑袋。做法是拿验证集全部样本对跑一遍推理把正样本对的距离分布和负样本对的距离分布分别画成直方图。找两个分布的交叉点把阈值定在交叉点往负样本方向偏移一点的位置保证漏点率低于误点率。如果业务方对点错零容忍阈值要往正样本方向收紧代价是重试率升高。阈值动态化的思路也值得尝试。页面越复杂、候选块越多负样本对的数量越多极端距离值出现的概率就越大。按候选块数量分段设置阈值比一个固定阈值更稳。难例挖掘要常态化。每上线一轮把线上实际点击日志里模型距离小但用户没点的样本对捞回来人工确认后加入训练集。这类样本是模型最需要的难负样本。注意不要一次塞太多每轮新增几百个就够了塞多了模型会发生灾难性遗忘把之前学的形状能力冲淡。回放旧数据的验证习惯也要保留。每次更新模型后用上一轮的线上日志跑一遍离线推理对比新旧模型在同一批样本上的误点率和拒绝率差距在1%以内再放行上线。这个步骤帮我挡掉过两次因为数据增强过度导致回归的更新。我现在的习惯是训练脚本里固定随机种子每次跑完自动保存三样东西模型权重、验证集距离分布的直方图、当轮的标注版本号。等位差错时先复查这三样再调网络结构。希望这套流程对正在做点选识别的你也有帮助。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

2.8 配置项:constants/ 全局常量体系与 TaoToken 统一 Key 接入实践 2026/9/28 18:22:58

2.8 配置项:constants/ 全局常量体系与 TaoToken 统一 Key 接入实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
智能旅行助手Agent实战:用TaoToken统一Key打通前后端分离的多Agent系统 2026/9/28 18:22:58

智能旅行助手Agent实战:用TaoToken统一Key打通前后端分离的多Agent系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
UltraEditor 替换公式正则表达式:用 TaoToken 统一 Key 打通 AI 辅助批量改写 2026/9/28 18:22:58

UltraEditor 替换公式正则表达式:用 TaoToken 统一 Key 打通 AI 辅助批量改写

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Claude Code 解析:从 Agent Loop 到 QueryEngine 的配置骨架 2026/9/28 18:22:58

Claude Code 解析:从 Agent Loop 到 QueryEngine 的配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
【C语言/数据结构】零基础打造控制台游戏:贪吃蛇实战教程----链表与Win32 API的完美结合! 2026/9/28 18:22:58

【C语言/数据结构】零基础打造控制台游戏:贪吃蛇实战教程----链表与Win32 API的完美结合!

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
基于 LangChain4j + LangGraph4j 的 Java 低代码智能体平台架构落地实践 2026/9/28 18:22:52

基于 LangChain4j + LangGraph4j 的 Java 低代码智能体平台架构落地实践

在 Java 生态里做 AI 应用开发,这两年有一个特别明显的感受:LangChain4j 和 LangGraph4j 的组合,正在把“智能体开发”从纯手工编码推向下一个阶段。我见过太多团队在同一个问题上反复踩坑——模型调用、记忆管理、工具编排、状态流转&#x…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉