Nodejs也能写Agent - 17.LangGraph篇 - Checkpointer 与 Memory

上一篇我们把条件边、ReAct 环和 Command.goto 啃完了------图已经能自己转圈。但说句扎心的:你一关进程,State 全没;想聊第二轮,还得亲手把整段 messages 再塞回去;想「发邮件前先让人点个批准」?图停都停不住。

真正让 Agent 从 demo 能跑 迈向 能上线 的关键基建,叫 Checkpointer :在每个 super-step 把 State 打成存档点,再用 thread_id 当游标指回那份存档。这一篇我们就把它讲透:MemorySaver、生产级 PostgresSaver,以及 interrupt + Command({ resume }) 的人机协作。

老规矩,本文 API 均以官网最新文档核对过(PersistenceAdd memoryInterruptsCheckpointers)。网上教程里 Annotation.Root / MessagesAnnotation 仍大量存在------那是 Legacy;本篇继续走 StateSchema。interrupt 也不要 当异常去 try/catch,官网主路径是读返回值里的 __interrupt__

一、先分清:Checkpointer ≠ Store

官网把持久化拆成两套互补系统,别混为一谈:

Checkpointer Store
存什么 图的 State 快照(checkpoint) 你自定义的 KV 数据
作用域 单个 thread 跨 thread
记忆类型 短记忆(会话内) 长记忆(用户偏好、事实库)
典型用途 多轮对话、HITL、时光旅行、故障恢复 跨会话记住「用户爱吃辣」
怎么用 compile({ checkpointer }) + thread_id compile({ store }),节点里读写

本篇主讲 Checkpointer。 Store / 长期记忆点到为止,后面专门篇再展开------先把「同一会话里不丢状态、能暂停能恢复」打牢。

二、为何需要 Checkpointer

没有它,每次 invoke 都是一张白纸:

需求 无 Checkpointer 有 Checkpointer
多轮对话 每次手动传入完整 history 同一 thread_id 自动累积 messages
进程重启 State 当场蒸发 从最近 checkpoint 接着跑
Human-in-the-loop 中间没法「暂停等人」 interrupt() 停住,人点完再 resume
调试 / 时光旅行 无法回溯 getState / getStateHistory 可查快照
生产部署 只适合玩具 Postgres 等落库实现

一句话:Checkpointer 是 Agent 会话记忆 + 暂停点的基础设施。
#mermaid-svg-MSNR0N6LhRLQijGA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MSNR0N6LhRLQijGA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MSNR0N6LhRLQijGA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MSNR0N6LhRLQijGA .error-icon{fill:#552222;}#mermaid-svg-MSNR0N6LhRLQijGA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MSNR0N6LhRLQijGA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MSNR0N6LhRLQijGA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MSNR0N6LhRLQijGA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MSNR0N6LhRLQijGA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MSNR0N6LhRLQijGA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MSNR0N6LhRLQijGA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MSNR0N6LhRLQijGA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MSNR0N6LhRLQijGA .marker.cross{stroke:#333333;}#mermaid-svg-MSNR0N6LhRLQijGA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MSNR0N6LhRLQijGA p{margin:0;}#mermaid-svg-MSNR0N6LhRLQijGA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MSNR0N6LhRLQijGA .cluster-label text{fill:#333;}#mermaid-svg-MSNR0N6LhRLQijGA .cluster-label span{color:#333;}#mermaid-svg-MSNR0N6LhRLQijGA .cluster-label span p{background-color:transparent;}#mermaid-svg-MSNR0N6LhRLQijGA .label text,#mermaid-svg-MSNR0N6LhRLQijGA span{fill:#333;color:#333;}#mermaid-svg-MSNR0N6LhRLQijGA .node rect,#mermaid-svg-MSNR0N6LhRLQijGA .node circle,#mermaid-svg-MSNR0N6LhRLQijGA .node ellipse,#mermaid-svg-MSNR0N6LhRLQijGA .node polygon,#mermaid-svg-MSNR0N6LhRLQijGA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MSNR0N6LhRLQijGA .rough-node .label text,#mermaid-svg-MSNR0N6LhRLQijGA .node .label text,#mermaid-svg-MSNR0N6LhRLQijGA .image-shape .label,#mermaid-svg-MSNR0N6LhRLQijGA .icon-shape .label{text-anchor:middle;}#mermaid-svg-MSNR0N6LhRLQijGA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MSNR0N6LhRLQijGA .rough-node .label,#mermaid-svg-MSNR0N6LhRLQijGA .node .label,#mermaid-svg-MSNR0N6LhRLQijGA .image-shape .label,#mermaid-svg-MSNR0N6LhRLQijGA .icon-shape .label{text-align:center;}#mermaid-svg-MSNR0N6LhRLQijGA .node.clickable{cursor:pointer;}#mermaid-svg-MSNR0N6LhRLQijGA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MSNR0N6LhRLQijGA .arrowheadPath{fill:#333333;}#mermaid-svg-MSNR0N6LhRLQijGA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MSNR0N6LhRLQijGA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MSNR0N6LhRLQijGA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MSNR0N6LhRLQijGA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MSNR0N6LhRLQijGA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MSNR0N6LhRLQijGA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MSNR0N6LhRLQijGA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MSNR0N6LhRLQijGA .cluster text{fill:#333;}#mermaid-svg-MSNR0N6LhRLQijGA .cluster span{color:#333;}#mermaid-svg-MSNR0N6LhRLQijGA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MSNR0N6LhRLQijGA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MSNR0N6LhRLQijGA rect.text{fill:none;stroke-width:0;}#mermaid-svg-MSNR0N6LhRLQijGA .icon-shape,#mermaid-svg-MSNR0N6LhRLQijGA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MSNR0N6LhRLQijGA .icon-shape p,#mermaid-svg-MSNR0N6LhRLQijGA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MSNR0N6LhRLQijGA .icon-shape .label rect,#mermaid-svg-MSNR0N6LhRLQijGA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MSNR0N6LhRLQijGA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MSNR0N6LhRLQijGA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MSNR0N6LhRLQijGA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} invoke round1
checkpoint
invoke same thread
checkpoint
interrupt
等待人审批
Command resume
继续并存档

三、MemorySaver:开发环境首选

MemorySaver 把 checkpoint 放在进程内存里,装包即用、零依赖:

typescript 复制代码
import {
  MemorySaver,
  StateGraph,
  StateSchema,
  MessagesValue,
  GraphNode,
  START,
  END,
} from "@langchain/langgraph";
import { ChatOllama } from "@langchain/ollama";
import { HumanMessage } from "@langchain/core/messages";

const llm = new ChatOllama({ model: "qwen2.5:7b", temperature: 0 });

const State = new StateSchema({
  messages: MessagesValue,
});

const agentNode: GraphNode<typeof State> = async (state) => {
  const response = await llm.invoke(state.messages);
  return { messages: [response] }; // 只返回增量
};

const checkpointer = new MemorySaver();

const graph = new StateGraph(State)
  .addNode("agent", agentNode)
  .addEdge(START, "agent")
  .addEdge("agent", END)
  .compile({ checkpointer }); // 关键:compile 时注入

⚠️ 进程一重启,MemorySaver 里的数据全部清空。 本地调试、单测、跟教程跑通 ------ 绝佳;生产请换落库实现。

想本地也落盘、又不想立刻上 Postgres?官还提到 SqliteSaver (文件型),心智模型一样:换 checkpointer 实现,invoke 接口不变。本系列生产示例以 Postgres 为主。

四、thread_id:持久化游标

启用 Checkpointer 之后,每次调用都要带上会话钥匙:

typescript 复制代码
const config = { configurable: { thread_id: "user-alice-session-1" } };

可以把它想成游戏存档槽位编号:

  • 同一个 thread_id:加载上一份 State,messages 继续累积
  • 换一个新 thread_id:全新空会话,跟旧槽无关
typescript 复制代码
const config = { configurable: { thread_id: "demo-thread-1" } };

// 第一轮
await graph.invoke(
  { messages: [new HumanMessage("记住:我的宠物叫小白。")] },
  config
);

// 第二轮:同一 thread,不用手动传历史
const result = await graph.invoke(
  { messages: [new HumanMessage("我的宠物叫什么?")] },
  config
);

console.log(result.messages.at(-1)?.content);
// 期望提到「小白」

想确认当前线程卡在哪、State 长什么样,可以读快照:

typescript 复制代码
const snapshot = await graph.getState(config);
console.log(snapshot.values);      // 当前 State
console.log(snapshot.next);        // 下一步会跑哪些节点
console.log(snapshot.tasks);       // 含未完成的 interrupt 任务时能看出来

Postgres 下 thread_id 建议 控制在 255 字符以内(列长度有限)。生产里用 UUID 或短哈希当会话 ID 最省心。

五、生产:PostgresSaver

包要单独装:

bash 复制代码
npm install @langchain/langgraph-checkpoint-postgres

三步走:fromConnStringsetup()compile

typescript 复制代码
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";

const checkpointer = PostgresSaver.fromConnString(
  process.env.DATABASE_URL! // 例如 postgresql://user:pass@localhost:5432/agent
);

// 首次使用必须建表;表已存在时再跑通常也安全,但别忘了这一步
await checkpointer.setup();

const graph = workflow.compile({ checkpointer });

MemorySaverinvoke 姿势完全一样------换实现、不换业务代码。这正是 Checkpointer 抽象的价值:本地内存、文件 Sqlite、Postgres,上层图逻辑一行都不用改。

六、interrupt:人机协作(HITL)

有时节点跑到一半必须等人:发邮件前审批、扣款前二次确认、改 Prompt 后再继续。官网做法是节点里调用 interrupt(payload)

  1. 图在此处暂停,State 由 Checkpointer 落盘
  2. payload(任意 JSON 可序列化值)出现在调用方返回的 __interrupt__
  3. 外部用相同 thread_idinvoke(new Command({ resume: value }))
  4. value 变成节点里 interrupt(...)返回值,节点继续往下走

⚠️ 与上一篇区分清楚:Command({ goto, update })节点函数的返回值 (图内路由);Command({ resume })invoke / stream 的输入 (从图外恢复 interrupt)。多轮普通聊天请继续传普通 State 增量(比如新的 messages),别拿 Command({ update }) 当输入凑合。

完整审批流示例

typescript 复制代码
import {
  StateGraph,
  StateSchema,
  GraphNode,
  MemorySaver,
  interrupt,
  Command,
  START,
  END,
} from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  emailDraft: z.string(),
  status: z.string().default("pending"),
});

const approveNode: GraphNode<typeof State> = async (state) => {
  // 暂停;payload 会出现在 result.__interrupt__
  const approved = interrupt({
    question: "是否发送以下邮件?",
    draft: state.emailDraft,
  });

  // resume 传入的值会成为上面的 approved
  if (approved) {
    // 真正发信......(注意:resume 时节点会从头再跑,副作用要幂等)
    return { status: "sent" };
  }
  return { status: "cancelled" };
};

const checkpointer = new MemorySaver();

const graph = new StateGraph(State)
  .addNode("approve", approveNode)
  .addEdge(START, "approve")
  .addEdge("approve", END)
  .compile({ checkpointer }); // interrupt 必须有 checkpointer

const config = { configurable: { thread_id: "email-approval-1" } };

// ① 首次 invoke:命中 interrupt,图暂停(不是抛异常)
const paused = await graph.invoke(
  { emailDraft: "您好,这是会议提醒。" },
  config
);

console.log(paused.__interrupt__);
// [{ value: { question: '是否发送以下邮件?', draft: '...' }, ... }]

// ② 用户在 UI 点「批准」后,用同一 thread_id resume
const final = await graph.invoke(new Command({ resume: true }), config);
console.log(final.status); // "sent"

关键心智模型:

要点 说明
前置条件 必须 compile({ checkpointer }),且 invoke 带 thread_id
怎么知道暂停了 看返回值的 __interrupt__不要默认当成 throw
resume 返回值 Command({ resume: X })X = 节点内 interrupt() 的返回值
节点重跑 resume 时从该节点开头重新执行interrupt 之前的代码会再跑一遍
幂等 interrupt 前若写了 DB / 发了半截请求,可能重复;副作用放到 interrupt 之后,或做成幂等

Human Checkpointer Graph App Human Checkpointer Graph App #mermaid-svg-jTR8KtJRgSdtZkV8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jTR8KtJRgSdtZkV8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jTR8KtJRgSdtZkV8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jTR8KtJRgSdtZkV8 .error-icon{fill:#552222;}#mermaid-svg-jTR8KtJRgSdtZkV8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jTR8KtJRgSdtZkV8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jTR8KtJRgSdtZkV8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jTR8KtJRgSdtZkV8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jTR8KtJRgSdtZkV8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jTR8KtJRgSdtZkV8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jTR8KtJRgSdtZkV8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jTR8KtJRgSdtZkV8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jTR8KtJRgSdtZkV8 .marker.cross{stroke:#333333;}#mermaid-svg-jTR8KtJRgSdtZkV8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jTR8KtJRgSdtZkV8 p{margin:0;}#mermaid-svg-jTR8KtJRgSdtZkV8 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jTR8KtJRgSdtZkV8 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-jTR8KtJRgSdtZkV8 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jTR8KtJRgSdtZkV8 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-jTR8KtJRgSdtZkV8 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-jTR8KtJRgSdtZkV8 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-jTR8KtJRgSdtZkV8 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-jTR8KtJRgSdtZkV8 .sequenceNumber{fill:white;}#mermaid-svg-jTR8KtJRgSdtZkV8 #sequencenumber{fill:#333;}#mermaid-svg-jTR8KtJRgSdtZkV8 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-jTR8KtJRgSdtZkV8 .messageText{fill:#333;stroke:none;}#mermaid-svg-jTR8KtJRgSdtZkV8 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jTR8KtJRgSdtZkV8 .labelText,#mermaid-svg-jTR8KtJRgSdtZkV8 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-jTR8KtJRgSdtZkV8 .loopText,#mermaid-svg-jTR8KtJRgSdtZkV8 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-jTR8KtJRgSdtZkV8 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jTR8KtJRgSdtZkV8 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-jTR8KtJRgSdtZkV8 .noteText,#mermaid-svg-jTR8KtJRgSdtZkV8 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-jTR8KtJRgSdtZkV8 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jTR8KtJRgSdtZkV8 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jTR8KtJRgSdtZkV8 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jTR8KtJRgSdtZkV8 .actorPopupMenu{position:absolute;}#mermaid-svg-jTR8KtJRgSdtZkV8 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-jTR8KtJRgSdtZkV8 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jTR8KtJRgSdtZkV8 .actor-man circle,#mermaid-svg-jTR8KtJRgSdtZkV8 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-jTR8KtJRgSdtZkV8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} invoke draft + thread_id 保存 checkpoint 返回 interrupt 展示草稿等待审批 批准 true invoke Command resume + 同 thread_id 加载 checkpoint status sent

常见坑

  1. 启用了 checkpointer 却不传 thread_id:无法稳定落盘 / 恢复,多轮像失忆,interrupt 也接不上。
  2. MemorySaver 直接上生产 :重启丢会话。生产用 PostgresSaver(或其它落库实现);本地持久化可考虑 Sqlite。
  3. PostgresSaver 忘记 await checkpointer.setup():表不存在,首次跑直接炸。
  4. resume 换了另一个 thread_id:等于去另一档存档找进度,恢复必然失败。
  5. interrupt 前有副作用:节点从头重跑会重复执行。把「真正有副作用的动作」放在 interrupt 之后,或做幂等键。
  6. 把 interrupt 当异常 try/catch :官网主路径是正常返回 + __interrupt__。别一上来就按 GraphInterrupt 异常流去写,容易跟当前 JS 文档对不上。
  7. thread_id 过长(Postgres):超过约 255 字符可能触发数据库列错误,用 UUID 更稳。
  8. 普通多轮对话误用 Command({ update }) 当输入 :继续聊天传普通 input 对象;resume 专用 Command 留给 interrupt。

小结

到这里,LangGraph 基础四件套齐了:

  • Checkpointer :每个 super-step 打存档;compile({ checkpointer }) 注入。
  • thread_id :会话游标;同 ID 累积,异 ID 隔离;可用 getState 窥探快照。
  • MemorySaver / PostgresSaver(及 Sqlite) :开发内存 → 生产落库,接口一致;Postgres 记得 setup()
  • interrupt + Command({ resume }) :HITL 暂停与恢复;读 __interrupt__;注意节点重跑与幂等。
  • Store:跨线程长期记忆------本篇只划清边界,不展开。

Phase 1 的 LangGraph 地基可以收束了:核心概念 → State / Graph API → 条件分支与循环 → Checkpointer / HITL 。下一批主题转向站得更高一点的视角------Agent 到底有哪些类型、什么时候用 ReAct、什么时候该上多 Agent / 工作流

相关推荐
Sirius Wu1 小时前
OpenClaw Observability 并发安全(Per-Request Hook)完整详解
服务器·网络·人工智能·安全·ai·架构·aigc
盛夏绽放1 小时前
Hermes Agent 全栈安装、配置与Web端架构深度解析(官方命令版·最终优化)
前端·架构·hermes
早期的虫儿有鸟吃1 小时前
Vue3
前端·javascript·vue.js
AI科技星1 小时前
全域三极公理统一篇——所有分析、代数、拓扑、算子理论同源归一,回归0/1/∞创世本源闭环《全域数学vs传统数学:人类文明进阶200讲》第91讲
人工智能·线性代数·机器学习·数据挖掘·回归·乖乖数学·全域数学
iuu_star2 小时前
Python大模型智能学习平台——设计与实现(AI教学系统)
大数据·人工智能·python·学习
雪隐2 小时前
个人电脑玩AI-10让5060 Ti给你打工——ComfyUI,我差点被它劝退三次
人工智能·后端
Elastic 中国社区官方博客2 小时前
从多模态 LLM 中引导构建音频嵌入
大数据·人工智能·elasticsearch·搜索引擎·ai·全文检索·音视频
hqk2 小时前
我给 AI 造了一个原型工具:AIWireframeViewer V1
前端·ai编程·vibecoding
hai3152475432 小时前
AI会不会产生意识,从黄仁勋的论断,以物理底层原理来理一理
人工智能