离岗检测怎么实现?通过客流相机或 AI 摄像头识别岗位区域无人状态,再推送告警消息

岗位区域有没有人,要看一张图的检测结果,并且连续几帧都空,再由你的业务服务发出告警。客流相机上报的是人数和滞留,动检上报的是画面变化。三条结果各有接口。把其中一条的静默当成离岗,收银台空着的时候群里不会响,灯闪一下倒会把计数清掉。

夜班收银台空了十几分钟,列表里只剩动检

便利店规定收银岗营业时段留人。周二夜里,收银员去后仓补货,收银台空了十几分钟。第二天店长把回放拉给开发看,业务后台的消息列表里只有几条动检:冰柜灯亮起一次,门口有人路过一次。人离开岗位的那一段,没有任何一条写着「收银岗无人」。

开发当时的接法是:通道一段时间没有 videoMotion,就当作离岗。这个接法回答的是「画面有没有变化」。冰柜灯闪、门口有人走过,计时被清零,岗位其实是空的。收银员站着对账、画面几乎不动,计时却会涨上去。店长要的是岗位框里有没有员工,不是像素动了没有。

另一家门店已经装了客流相机。进店人数每天都有,人员滞留也会推上来。店长问:人数归零的时候能不能直接当离岗。可以往下做,但先把消息类型拆开。滞留是人停留过久,和岗位空着是相反的两件事。

一次调用只回答这一帧有没有员工

云端这条链路的方法名是 absenceDetect,说明在《离岗检测》。调用方上传一张图片,返回 detectResult:true 表示检测到员工、这一帧在岗;false 表示未检测到员工、这一帧离岗。它是同步接口,结果在当次响应里,不会自动变成一条设备告警。

《离岗检测》写明该能力是按需订阅的增值服务。测试额度通过工单或商务咨询申请,验证后再付费使用。没开通时接口会失败,失败不等于现场有人,也不等于现场无人。

入参里和岗位相关的是这些:

参数 文档口径
token 管理员 accessToken
type "0" 图片 URL,"1" Base64
content 与 type 对应的地址或 Base64
threshold 检测阈值,范围 (0, 1]。文档样例为 0.8
detectRegions 可选。最多 3 个区域,每个区域 3 到 6 个点,坐标归一化到 0, 1,再转成该区域的最大外接矩形
repositoryId 可选。人形工装检测目标库 ID。传入后,从该库判断图中有没有匹配工装的人员

区域要画在岗位上,例如收银台内侧,而不是整间店。框进过道,路过的人会把这一帧打成在岗。需要把「任意被检出的员工」收紧到「指定工装」时,再传 repositoryId。库的创建和注册在《AI服务说明》里,方法是 createAiDetectRepository、addAiDetectTarget。不传该字段时,按《离岗检测》的返回说明理解 detectResult。顾客站进框内会不会被算成在岗,用开通后的回放核对,不要靠改告警文案弥补。

单帧 false 还不是店长要的那条消息。员工弯腰、被货架挡住、低头看手机,都可能让某一帧检不出人。业务侧自己保留连续空帧计数,达到约定次数再通知。次数和间隔是岗位配置,接口没有默认值班表。

图片从设备来。性能较好的设备用 setDeviceSnapEnhanced:客户端请求间隔大于等于 1 秒,短于 1 秒会拿到上一张图;返回的 url 2 小时内有效。2017 年前上市、处理抓图较慢的设备,文档建议改用 setDeviceSnap,间隔大于等于 3 秒。用哪一个,以该设备实测为准。type 为 "0" 时,把这次返回的 url 填进 absenceDetect 的 content,并在有效期内完成检测。图片只在内网能打开时,改为 type "1",content 传 Base64。

《AI服务说明》把 absenceDetect 放在行业场景一组,同组还有抽烟、玩手机、货架、垃圾满溢、工装和热力。humanDetect 是另一条人形分析,返回字段不同,不能拿来代替 detectResult。

人数、滞留、人形、空岗,各看各的结果

动手前用四次核对决定走抓图检测,还是走客流消息。

核对服务开了没有。 absenceDetect 未开通,抓图成功也得不到 detectResult。先申请额度,用一张在岗图和一张空岗图各调一次,确认 true / false 与肉眼一致,再接入值班循环。

核对这一帧的图是不是岗位。 detectRegions 只覆盖岗位。全幅画面会把过道、门口算进去。点位不足 3 个,或超过 6 个,按《离岗检测》的区域说明修正后再测。阈值从样例 0.8 起步,用误报和漏报回放调整,范围仍是 (0, 1]。

核对客流相机能回答的是哪一种人数。 《设备能力集》里,NumberStat 是过线客流量,ManNumDec 是区域客流量。过线统计的是进出,岗位上站着不动的人不会形成一条新的过线。区域客流才和「这块区域里有没有人」接近。II 类能力在单通道设备上标在设备能力集,在多通道设备上标在通道能力集。通道字符串里没有 ManNumDec,就不要用客流消息做这个岗位的空岗判断。有没有这个能力,以 listDeviceDetailsByPage 返回的 deviceAbility、channelAbility 为准。本文不按型号承诺某台相机一定带区域客流或一定能抓图。

核对推上来的消息类型。 订阅大类 callbackFlag=numberstat 之后,《事件消息类型定义》里对应两种 msgType:numberstat 是客流量上报,manyStay 是人员滞留。滞留表示人留在区域里,不能累加进空岗计数。人数字段以《事件消息格式定义》里该类型的样例为准,联调时先落原始报文再映射。映射完成之前,人数持续为 0 的判断先不要上线。

设备告警是第三条线,大类是 alarm。human 是人形检测报警,表示检出了人。videoMotion 是动态检测,表示画面有变化。一段时间内没有 human,可能是岗位真的空了,也可能是角度看不到、策略没开,或回调里根本没订 alarm。它可以给回放对照,不能单独作为离岗结论。HeaderDetect(人头检测)同样只说明设备具备人头检测能力,不是空岗结果。

空岗告警在业务里成立,需要同时满足:当前时刻落在该岗位的值班时段内;这一帧的抓图和检测都成功;detectResult === false;这样的空帧连续达到岗位配置的次数。抓图失败、设备离线、检测接口返回非 0,这一轮跳过,不增加空帧计数。

客流相机若已经能稳定给出区域内人数,空岗也可以定义成:值班时段内,区域人数按报文持续为 0,达到同一套连续次数。这仍然是业务判断。平台没有把 numberstat 定义成离岗告警。

服务开通并且通道在线,再决定抓图还是订客流

设备、抓图、客流消息和图片检测走同一套开放接口。应用在乐橙云开放平台创建之后,在控制台「我的应用 → 应用信息」取得 appId 和 appSecret,只放在服务端。设备添加到这个开发者账号,通道在线,后续查询才有对象。接入额度以控制台「我的资源」为准。

请求统一是 HTTP POST,地址形如 https://openapi.lechange.cn/openapi/{method}。请求体分 system、id、params。签名按《开发规范》:原始串 time:{time},nonce:{nonce},appSecret:{appSecret};password 为 appSecret 的 SHA-256 小写十六进制;sign 为 HMAC-SHA256 后再 Base64。time 与服务器相差超过 5 分钟返回 SN1002,nonce 在 5 分钟内重复返回 SN1005。管理员 token 用方法 accessToken 获取,expireTime 单位是秒,按返回值缓存。过期或不存在时返回 TK1002。

《离岗检测》要求管理员 accessToken。抓图接口允许子账号,最小权限是 Capture,资源形式为 cam:序列号:通道号。空岗这条链路用管理员 token 即可,浏览器不保存 token。

岗位本身写在业务配置里:哪台设备的哪个通道、框的四个点、值班时段、连续几次空帧才通知。平台设备列表没有「收银岗」这个对象。listDeviceDetailsByPage 用来核对序列号、通道号、在线状态和能力集,page 从 1 开始,pageSize 为 1~50。

设备侧消息如果也要进同一套后台,用 setMessageCallback 设置账号的回调。status 为 on 时,callbackUrl 和 callbackFlag 必填。地址必须能从外网访问。callbackFlag 可组合 alarm、deviceStatus、numberstat、iot、faceAnalysis,逗号分隔。客流必须包含 numberstat,只订 alarm 收不到客流。basePush 为 "1" 时推送该账号下关联的乐橙 App 设备消息,"2" 为不推送,默认是 "2"。开发者后台若不需要 App 侧消息,保持 "2"。

这个接口设置的是开发者账号当前这一份回调。改之前先 getMessageCallback,把仍要接收的大类拼进同一次 callbackFlag。只提交 numberstat,原先的 alarm 不会自动留下。控制台「消息推送服务」和这个接口是同一份配置,两边不要各改各的。回调地址暂时无法对外时,可查《消息通道订阅(拉取模式)》的 pullMessages。拉取到的 msgBody 与推送载荷一致,里面同样没有 absenceDetect 的返回值。空岗仍要自己定时抓图检测。

text 复制代码
业务配置:deviceId + channelId + 岗位框 + 值班时段
                    │
                    ▼
            deviceOnline 在线?
              │否                │是
              ▼                  ▼
         本轮跳过        setDeviceSnapEnhanced
         不计空岗               │失败(DV1007 / DV1053 / DV1010)
                                ▼
                           本轮跳过,不计空岗
                                │成功,url 2 小时内有效
                                ▼
                         absenceDetect
                                │
              detectResult true ┼ false
                   计数清零     │  计数 +1,到阈值后业务服务发告警
                                │
        另一路(可选):callbackFlag 含 numberstat
        msgType=numberstat 按格式页映射人数
        msgType=manyStay 只归档,不进空岗计数

告警文案、接收人、电话或短信,由业务服务在连续判定成立时发送。《离岗检测》的返回体里没有通知对象。不要等 getAlarmMessage 里出现一条类型叫离岗的记录。那组接口查询的是设备告警。

连续空帧之后,由业务服务通知店长

下面是 Node.js 18+。岗位表、连续次数和轮询间隔都是业务参数。示例用 60 秒一轮、连续 3 次空帧再通知,大约 3 分钟。间隔必须大于抓图接口的最小间隔,否则 DV1010(1 秒内同一设备多次抓图)会失败,短于 1 秒时增强抓图还可能重复返回上一张图,把同一帧算成多次空岗。

一次轮询至少两次开放接口调用:抓图和检测。另加在线状态查询时是三次。《全局返回码说明》写明:未付费账号每分钟 20 次、每天 10000 次;付费账号每分钟 500 次、每天 250000 次。超出分别是 OP1039、OP1040。岗位变多时把各岗位的启动时间错开,而不是把间隔压到 1 秒。

javascript 复制代码
const crypto = require('crypto');
const http = require('http');

const OPENAPI_BASE = 'https://openapi.lechange.cn/openapi';

const posts = {
  CASHIER_01: {
    name: '湖滨店收银岗',
    deviceId: 'DEVICE_SERIAL',
    channelId: '0',
    snapMethod: 'setDeviceSnapEnhanced',
    threshold: 0.8,
    // 可选。需要按工装库判断时再填 createAiDetectRepository 返回的库 ID
    repositoryId: '',
    region: [
      { x: 0.42, y: 0.28 },
      { x: 0.74, y: 0.28 },
      { x: 0.74, y: 0.70 },
      { x: 0.42, y: 0.70 }
    ],
    dutyStart: '08:00',
    dutyEnd: '22:00',
    missLimit: 3,
    missStreak: 0,
    alerted: false
  }
};

function calcSign(time, nonce, appSecret) {
  const raw = `time:${time},nonce:${nonce},appSecret:${appSecret}`;
  const password = crypto.createHash('sha256').update(appSecret, 'utf8').digest('hex');
  return crypto.createHmac('sha256', password).update(raw, 'utf8').digest('base64');
}

let adminToken = { value: '', expireAt: 0 };

async function callOpenApi(method, appId, appSecret, params, allowRetry = true) {
  const time = Math.floor(Date.now() / 1000);
  const nonce = crypto.randomUUID();
  const res = await fetch(`${OPENAPI_BASE}/${method}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      system: {
        ver: '1.0',
        appId,
        sign: calcSign(time, nonce, appSecret),
        time,
        nonce
      },
      id: crypto.randomUUID(),
      params
    })
  });
  const json = await res.json();
  const code = json?.result?.code;
  if (code === 'TK1002' && allowRetry && method !== 'accessToken') {
    adminToken = { value: '', expireAt: 0 };
    const token = await getAdminToken(appId, appSecret);
    return callOpenApi(method, appId, appSecret, { ...params, token }, false);
  }
  if (code !== '0') {
    const err = new Error(json?.result?.msg || 'openapi error');
    err.code = code;
    throw err;
  }
  return json.result.data || {};
}

async function getAdminToken(appId, appSecret) {
  const now = Math.floor(Date.now() / 1000);
  if (adminToken.value && adminToken.expireAt - now > 3600) return adminToken.value;
  const data = await callOpenApi('accessToken', appId, appSecret, {});
  adminToken = {
    value: data.accessToken,
    expireAt: now + Number(data.expireTime || 0)
  };
  return adminToken.value;
}

function minutesInShanghai(date) {
  const parts = new Intl.DateTimeFormat('en-GB', {
    timeZone: 'Asia/Shanghai',
    hour: '2-digit',
    minute: '2-digit',
    hourCycle: 'h23'
  }).formatToParts(date);
  const pick = (type) => parts.find((item) => item.type === type).value;
  return Number(pick('hour')) * 60 + Number(pick('minute'));
}

function inDuty(post, date) {
  const toMin = (hm) => {
    const [h, m] = hm.split(':').map(Number);
    return h * 60 + m;
  };
  const cur = minutesInShanghai(date);
  const start = toMin(post.dutyStart);
  const end = toMin(post.dutyEnd);
  if (start <= end) return cur >= start && cur < end;
  return cur >= start || cur < end;
}

function hasAbility(deviceAbility, channelAbility, code) {
  return `${deviceAbility || ''},${channelAbility || ''}`
    .split(',')
    .map((item) => item.trim())
    .includes(code);
}

async function channelReady(appId, appSecret, post) {
  const token = await getAdminToken(appId, appSecret);
  const data = await callOpenApi('deviceOnline', appId, appSecret, {
    token,
    deviceId: post.deviceId
  });
  if (String(data.onLine) !== '1') return false;
  const channel = (data.channels || []).find((item) => {
    return String(item.channelId) === String(post.channelId);
  });
  return !!channel && String(channel.onLine) === '1';
}

async function pollPost(appId, appSecret, post) {
  if (!inDuty(post, new Date())) return { state: 'off_duty_hours' };
  const online = await channelReady(appId, appSecret, post);
  if (!online) return { state: 'skip', reason: 'offline' };

  const token = await getAdminToken(appId, appSecret);
  let snap;
  try {
    snap = await callOpenApi(post.snapMethod, appId, appSecret, {
      token,
      deviceId: post.deviceId,
      channelId: String(post.channelId)
    });
  } catch (err) {
    return { state: 'skip', reason: err.code || 'snap_failed' };
  }
  if (!snap.url) return { state: 'skip', reason: 'no_url' };

  const params = {
    token,
    type: '0',
    content: snap.url,
    threshold: post.threshold,
    detectRegions: [post.region]
  };
  if (post.repositoryId) params.repositoryId = post.repositoryId;

  const detected = await callOpenApi('absenceDetect', appId, appSecret, params);
  if (typeof detected.detectResult !== 'boolean') {
    return { state: 'skip', reason: 'no_detect_result' };
  }
  if (detected.detectResult) {
    post.missStreak = 0;
    post.alerted = false;
    return { state: 'present' };
  }

  post.missStreak += 1;
  if (post.missStreak >= post.missLimit && !post.alerted) {
    post.alerted = true;
    // 抓图 url 2 小时内有效。通知里要附图时,这里先下载转存。
    console.log(JSON.stringify({
      event: 'absence',
      name: post.name,
      deviceId: post.deviceId,
      channelId: post.channelId,
      streak: post.missStreak,
      frameUrl: snap.url,
      at: new Date().toISOString()
    }));
    return { state: 'alert' };
  }
  return { state: 'empty', streak: post.missStreak };
}

async function ensureCallback(appId, appSecret, callbackUrl) {
  const token = await getAdminToken(appId, appSecret);
  const current = await callOpenApi('getMessageCallback', appId, appSecret, { token });
  const flags = new Set(
    String(current.callbackFlag || '')
      .split(',')
      .map((item) => item.trim())
      .filter(Boolean)
  );
  flags.add('alarm');
  flags.add('deviceStatus');
  flags.add('numberstat');
  await callOpenApi('setMessageCallback', appId, appSecret, {
    token,
    status: 'on',
    callbackUrl,
    callbackFlag: [...flags].join(','),
    basePush: '2'
  });
}

function startCallback(port) {
  const server = http.createServer((req, res) => {
    const chunks = [];
    req.on('data', (chunk) => chunks.push(chunk));
    req.on('end', () => {
      res.writeHead(200, { 'Content-Type': 'text/plain' });
      res.end('ok');
      if (req.method !== 'POST') return;
      let msg;
      try {
        msg = JSON.parse(Buffer.concat(chunks).toString('utf8'));
      } catch (err) {
        return;
      }
      const list = Array.isArray(msg) ? msg : [msg];
      for (const item of list) {
        if (item.msgType === 'manyStay') {
          console.log(JSON.stringify({ keep: 'stay', did: item.did || item.deviceId }));
          continue;
        }
        if (item.msgType === 'numberstat') {
          console.log(JSON.stringify({ keep: 'footfall', body: item }));
        }
      }
    });
  });
  server.listen(port);
}

async function loop(appId, appSecret) {
  for (const post of Object.values(posts)) {
    try {
      const result = await pollPost(appId, appSecret, post);
      console.log(post.name, result.state, result.reason || result.streak || '');
    } catch (err) {
      console.log(post.name, 'skip', err.code || err.message);
    }
  }
}

module.exports = {
  calcSign,
  pollPost,
  ensureCallback,
  startCallback,
  loop,
  hasAbility
};

进程重启会丢掉内存里的 missStreak 和 alerted。生产把这两项放到岗位记录里,避免重启后立刻再报一次,或把已经累积的空帧清零。hasAbility 留给客流方案:上线前确认目标通道含 ManNumDec。过线能力 NumberStat 只说明能做进出统计。

回调处理函数先返回 HTTP 200,再解析报文。《平台主动推送(推送模式)》写明:多次不返回 200,开放平台将不再向该地址推送。解析失败也要先应答。manyStay 只归档。numberstat 先存原文,人数字段对照《事件消息格式定义》映射之后,才能复用上面的连续计数。

设备告警需要回放时,用 getAlarmMessage。入参包含 deviceId、channelId、beginTime、endTime(yyyy-MM-dd HH:mm:ss),count 范围 1~30,nextAlarmId 翻页。子账号最小权限是 Alarm。这里能看到 human、videoMotion 等设备消息,看不到 absenceDetect 的 detectResult。

这些标题和方法名可以按页核对

文档站按标题或方法名检索。下表只列本文用到的页。

文档标题 方法 用来核对什么
《开发规范》 accessToken 请求壳、签名三步、SN1002、SN1005
《accessToken:获取管理员token》 accessToken expireTime 单位为秒
《全局返回码说明》 --- TK1002、DV1010、DV1053、DV1007、OP1012、OP1039、OP1040
《AI服务说明》 absenceDetect 等 离岗检测属于按需开通的行业场景接口
《离岗检测》 absenceDetect type、content、threshold、detectRegions、repositoryId、detectResult
《setDeviceSnapEnhanced设备抓图》 setDeviceSnapEnhanced 间隔大于等于 1 秒,url 2 小时有效
《setDeviceSnap设备抓图》 setDeviceSnap 间隔大于等于 3 秒,老设备抓图
《设备操作说明》 --- 两个抓图方法的选用说明
《获取设备在线状态》 deviceOnline onLine:0 离线,1 在线,3 升级中,4 休眠
《分页查询设备详细信息》 listDeviceDetailsByPage deviceAbility、channelAbility、在线状态
《设备能力集》 --- NumberStat 过线客流,ManNumDec 区域客流,HeaderDetect 人头检测
《推送配置说明》 --- 回调设置与查询的模块入口
《设置报警回调地址》 setMessageCallback callbackFlag 含 alarm、numberstat 等
《查看报警回调地址》 getMessageCallback 核对当前 URL 和大类,避免覆盖
《平台主动推送(推送模式)》 --- 回调须返回 200,否则停止推送
《事件消息类型定义》 --- numberstat 客流量,manyStay 滞留,human 人形,videoMotion 动检
《事件消息格式定义》 --- 各 msgType 的字段样例
《消息通道订阅(拉取模式)》 pullMessages 无法提供公网回调时的拉取方式
《设备消息说明》 --- 设备告警查询这一组接口
《查询用户报警信息》 getAlarmMessage 查设备告警,不承载 detectResult

抓图失败被记成离岗,回调不回 200 被停推

现象 先查什么
接口失败,现场画面明明是空的或有人 离岗检测是否已开通。返回码对照《全局返回码说明》:权限不足是 OP1012,参数不合法是 OP1003。非 0 不要写成 detectResult=false
每一分钟都报一次离岗 单帧 false 被直接外推。加上连续次数,并且 alerted 在下一次 true 之前不要重复通知
人一直在,仍报离岗 框是否罩住岗位;员工是否被遮挡;阈值是否过高。需要工装口径时再传 repositoryId,用回放核对
人已经离开,迟迟不报 间隔和 missLimit 决定时延。抓图或检测失败会跳过本轮,计数不会增加。先看日志里的 skip 原因
灯一闪就不报,人站着不动却报 判定用了 videoMotion 的有无。改回 detectResult
收到滞留就通知离岗 manyStay 是人员滞留。空岗计数只接受你映射之后的区域人数为 0,或 detectResult===false
订了告警却没有客流 getMessageCallback 的 callbackFlag 是否包含 numberstat。只订 alarm 不会带来客流
改完回调,原来的人形也没了 setMessageCallback 按本次传入的整串生效。先读再合并
生产突然收不到设备消息 账号只有一份回调地址。测试 URL 覆盖了生产 URL。测试用独立应用
抓图报 DV1010 1 秒内同一设备抓了多次。拉长轮询间隔,岗位之间错开
抓图报 DV1007 或 DV1053 设备离线,或网络原因没返回图片。本轮跳过。deviceOnline 里 3 是升级中,4 是休眠,都不要抓
增强抓图总是同一张图 请求间隔小于 1 秒,接口返回上一张。空帧计数会被同一张图堆满
检测报 OP1003 区域点数不在 3~6,坐标不在 0, 1,或 threshold 不在 (0, 1]。type 与 content 也要成对
Base64 报 OP1030 《全局返回码说明》中该码是文件大小超出限制。缩小图片再传,或改回抓图 URL
返回 TK1002 管理员 token 过期或不存在,重新调用 accessToken。TK1003 则是 token 与 appId 不对应
返回 SN1001 签名与平台不一致,核对三段式和 SHA-256 小写十六进制
多岗位跑起来后出现 OP1039 或 OP1040 分钟频率或当日总次数到达《全局返回码说明》里的额度。加大间隔、错峰,或按该页说明扩展额度
getAlarmMessage 里没有离岗 预期如此。离岗结论在 absenceDetect 的响应里,由业务服务自己发
告警里的图片打不开 抓图 url 2 小时有效。通知需要留图时,在判定成立当时下载转存
回调一段时间后彻底没有设备消息 处理逻辑写在返回 200 之前,或经常超时。先应答,再落库。与抓图轮询无关,轮询不依赖这条回调
子账号能抓图,检测却失败 检测文档要求管理员 token。抓图的子账号权限不能代替它

离场前用两段录像验收:员工离开岗位并超过设定的连续次数,业务通知只发一次,画面里能对上该岗位;员工回到框内,计数清零,下一次离开可以再次通知。抓图失败的那几分钟不产生通知。做到这三步,离岗告警就和动检、滞留分开了。

以设备能力集与开通服务为准。区域客流、抓图频率、工装库是否参与判断,都看该通道当前能力和离岗检测是否已开通,不随「摄像头已经在线」一起出现。

相关推荐
2301_776045231 小时前
山寨币与比特币之间的关系
大数据·人工智能·区块链
ahccqw3 小时前
OpenCV 缩放 与 位移
人工智能·opencv·计算机视觉
长不大的杨瑞杰3 小时前
OpenCode v2 GitLab 集成实战:Issue 驱动开发、MR 审查与 CI/CD 最小权限
人工智能·ci/cd·gitlab·ai编程·opencode
深蓝AI3 小时前
美团 LongCat-2.5 拆解:1.6T 参数只激活 3%,1M 上下文卖 0.3 美元,Agent 模型的账变了
人工智能·llm
小和尚同志8 小时前
54.6k star 的 Multica: 集多 Agent 和看板于一体的 AI 平台
人工智能·aigc·three.js
一枕槐安4879 小时前
少样本提示(Few-shot)在业务场景的落地实践
大数据·人工智能·意图分类·mmr多样性·knn检索·动态示例选择
ZhengEnCi9 小时前
模型训练完整流程:从数据准备到模型上线的 10 个关键步骤(全文可跑)
后端·算法
王中阳Go9 小时前
后端转 Agent 岗面试怎么答:8 个高频问题与应答口径
人工智能·后端·python·大语言模型
程序猿DD10 小时前
OctaFuse Gateway 2.18.0:MCP 接入支持 OAuth,供应商粘性绑定可视化管理
后端