状态机与思考循环 ——CogitoAgent开发实战(一)

状态机与思考循环

------CogitoAgent开发实战(一)

📖 本文是专栏《让大模型真正"活"在你电脑里------CogitoAgent开发实战》的第一篇。我们将一起思考一个问题:如何让一个AI程序既能在后台"自己琢磨事儿",又能随时响应你的指令?这就是状态机和思考循环要解决的核心问题。


📌 从一个生活场景开始

想象你有一个非常能干的私人助理。

平常的时候,他会自己在办公室里转悠------整理文件、翻阅资料、熟悉你的工作内容。你不需要时刻盯着他,他自己知道该做什么。

当你需要他时,你只需要喊一声,他就会立刻停下来,走到你面前,听你吩咐。你说完,他又回到自己的节奏里,继续忙活。

这就是 CogitoAgent 的工作模式。

技术翻译

  • "自己转悠" = THINKING 状态(AI主动探索)
  • "喊一声" = 按 Enter 打断
  • "听你吩咐" = AWAITING_INPUT 状态(等待用户输入)

这个机制看似简单,但实现起来有几个棘手的难题。


一、核心难题:AI的"自言自语"和"听你说话"不能打架

1.1 如果我们不做状态管理,会发生什么?

假设我们用一个最简单的 while 循环:

javascript 复制代码
// ❌ 错误示范
while(true) {
  const userInput = getUserInput();  // 等着用户打字
  const response = callAI(userInput);
  console.log(response);
}

这里有个致命问题:getUserInput()卡住整个程序。AI 只能在你输入之后才有反应,永远不可能主动做任何事情。

反过来,如果让 AI 持续运行:

javascript 复制代码
// ❌ 另一个错误示范
while(true) {
  const response = callAI();  // AI自己思考
  console.log(response);
  // 用户想说话?程序根本不给你机会
}

这样用户永远无法插嘴。

问题的本质 :我们需要一个程序,能同时做两件事------既能自己运转,又能随时响应外部输入。但在传统的同步编程里,程序一次只能做一件事。

1.2 Node.js 的解法:异步 + 事件驱动

Node.js 的核心优势是异步非阻塞。你可以这样理解:

  • 程序里有一个事件队列,放着各种待处理的事情(定时器到点了、用户按键盘了、网络请求回来了)
  • 主线程不断从这个队列里取任务执行
  • 如果一个任务需要等待(比如等用户输入),它不会卡住整个程序,而是把自己挂起,让其他任务先执行

CogitoAgent 正是利用了这一点。

两个核心机制

  1. 定时器:每 3 秒触发一次"让 AI 思考"的任务
  2. 事件监听:用户按 Enter 时,触发"处理用户输入"的任务

这两个任务不会同时执行 ,但它们可以交替执行------就像一个人可以一边吃饭一边看手机,虽然同一时刻只能做一件事,但切换得足够快,感觉就像同时在做。


二、状态机:用一个"模式开关"来管理行为

有了异步机制,我们还需要一个规则来决定:当前应该做什么?

这就是状态机。

2.1 两个状态的定义

javascript 复制代码
const STATE = {
  THINKING: 'THINKING',           // 模式A:AI自己思考
  AWAITING_INPUT: 'AWAITING_INPUT' // 模式B:等待用户输入
};

let state = STATE.THINKING;  // 启动后默认进入思考模式

你可以把 state 理解为一个模式开关

开关位置 程序行为
THINKING 定时器触发时,执行 thinkCycle()(AI思考一轮)
AWAITING_INPUT 定时器触发时,什么都不做(等待用户)

2.2 为什么需要两个状态?用一个布尔值不行吗?

你可能会想:用一个 isThinking 布尔值不就够了?

javascript 复制代码
let isThinking = true;  // true=思考中,false=等待输入

理论上可以,但随着逻辑变复杂,布尔值会带来困扰:

  • "思考中"状态下,用户打断后应该进入"等待输入"------布尔值从 truefalse,没问题
  • "等待输入"状态下,用户发了消息应该恢复思考------布尔值从 falsetrue,也没问题

那为什么还要用两个具名的状态常量?

原因一:可读性

javascript 复制代码
// 用布尔值
if (!isThinking) { ... }

// 用状态常量
if (state === STATE.AWAITING_INPUT) { ... }

后者一眼就能看懂是在检查"是否在等待用户输入",前者需要你记住 isThinking === false 是什么意思。

原因二:扩展性

如果将来需要第三个状态(比如"暂停""错误"等),布尔值就彻底不够用了。用状态常量,增加一个值即可。

原因三:防止歧义

布尔值无法表达"为什么会是这个状态"。具名的状态常量自带语义。

2.3 状态的转换规则

状态的转换不是随意的,有明确的规则:

复制代码
启动
  ↓
THINKING ──(用户按Enter)──→ AWAITING_INPUT
   ↑                            │
   └──(用户发送消息)────────────┘
   
THINKING ──(AI输出[WAIT])──→ AWAITING_INPUT

什么时候从 THINKING 变成 AWAITING_INPUT

两种情况:

  1. 用户主动打断:你按了 Enter
  2. AI 主动等待 :AI 在回复中写了 [WAIT],表示"我说完了,等你回话"

什么时候从 AWAITING_INPUT 变回 THINKING

用户发送了消息(可以是具体内容,也可以直接按 Enter 发空消息,表示"没事,你继续想")


三、思考循环:如何让AI"每3秒想一次"

3.1 问题:不用 while(true),怎么实现循环?

在普通编程里,想重复做一件事,我们会写:

javascript 复制代码
while(true) {
  doSomething();
  sleep(3000);  // 等3秒
}

但在 Node.js 里,没有 sleep() 函数(实际上有一个 setTimeout,但它不会像 sleep 那样阻塞程序)。更重要的是,如果我们用 while(true) 阻塞主线程,用户输入就永远得不到处理了。

解法:递归的 setTimeout

javascript 复制代码
function scheduleNext() {
  setTimeout(() => {
    doSomething();
    scheduleNext();  // 做完后,再安排下一次
  }, 3000);
}

这个模式的关键在于:setTimeout 只是"安排"一个任务在 3 秒后执行,安排完就立刻返回。主线程可以在这 3 秒里做其他事情(比如响应用户输入)。

3 秒后,doSomething() 被执行,执行完又调用 scheduleNext(),再次安排下一个 3 秒后的任务。

这就形成了一个永不阻塞、永不停止的循环。

3.2 为什么要先 clearTimeout

javascript 复制代码
let timer = null;

function scheduleNext() {
  clearTimeout(timer);  // 清除之前的定时器
  timer = setTimeout(() => {
    // ...
  }, 3000);
}

这个细节很重要。考虑一个场景:

  1. 第 0 秒:scheduleNext() 被调用,安排 3 秒后执行任务
  2. 第 1 秒:用户按 Enter 打断,我们调用了 scheduleNext()(想重新安排?或者只是重置?)
  3. 如果不先 clearTimeout,第 0 秒安排的那个定时器仍然存在,3 秒后(即第 3 秒)还会触发

这可能导致意料之外的任务执行clearTimeout 保证了:每次安排新任务之前,先把旧任务取消掉。确保只有最后一次安排会生效。

3.3 状态的"守卫":只在 THINKING 模式下执行

javascript 复制代码
function scheduleNext() {
  clearTimeout(timer);
  timer = setTimeout(() => {
    if (state === STATE.THINKING) {  // 守卫
      thinkCycle();
      scheduleNext();
    }
  }, 3000);
}

这个 if 检查至关重要。

当程序处于 AWAITING_INPUT 状态时,我们不希望 AI 继续思考。但这个定时器是已经安排好的,到点就会触发。守卫的作用就是:到点了,先看看当前是什么模式。如果是等待输入模式,就直接跳过,不执行思考,也不继续安排下一次循环。

这也意味着:当状态从 AWAITING_INPUT 切回 THINKING 时,需要主动 调用 scheduleNext() 来恢复循环。


四、用户打断:如何让AI"立刻闭嘴"

4.1 打断的挑战

用户按 Enter 时,AI 可能正在做两件事之一:

  • 正在思考thinkCycle() 还没开始,或者还没执行完
  • 正在等待 :状态是 AWAITING_INPUT,啥也没干

第二种情况很简单------本来就在等你,不需要打断。

第一种情况复杂:thinkCycle() 是一个异步函数,里面可能正在:

  • 等待 LLM 的流式响应(一个可能持续几秒到十几秒的网络请求)
  • 执行文件操作(读取大文件可能耗时)

我们希望:无论 AI 当前在做什么,用户按 Enter 后,它应该立即停止当前活动,进入等待输入模式。

4.2 解决方案:中断标志

javascript 复制代码
let shouldStop = false;  // 全局中断标志

这是一个共享变量,用户输入处理和思考循环都能访问到。

打断流程

  1. 用户按 Enter → handleUserInput 被调用
  2. handleUserInput 设置 shouldStop = true
  3. handleUserInput 清除定时器,防止下次循环启动
  4. handleUserInput 将状态改为 AWAITING_INPUT
  5. thinkCycle() 在执行过程中,不断检查 shouldStop,发现为 true 就立即退出

4.3 中断检查点

thinkCycle() 中,我们在关键位置检查中断标志:

javascript 复制代码
async function thinkCycle() {
  // 检查点1:函数开头(还没开始干活)
  if (shouldStop) return;
  
  for await (const chunk of streamChat(messages)) {
    // 检查点2:每收到一个响应块,都检查一次
    if (shouldStop) {
      shouldStop = false;  // 重置标志
      return;              // 立即退出
    }
    // 处理chunk...
  }
  
  // 检查点3:重要操作之间也可以加
  if (shouldStop) return;
  
  // 执行工具...
}

注意检查点2:LLM 的流式响应可能持续很长时间(比如生成几百字的回复)。我们在每个 chunk 到达时都检查中断标志,这样用户打断时,最多浪费一个 chunk 的处理,而不是等整个响应完成。

4.4 为什么需要 clearTimeout 配合?

设置 shouldStop = true 只能让正在执行thinkCycle() 退出。但定时器可能已经安排了下一次 thinkCycle()

假设:

  • 第 0 秒:scheduleNext() 安排了 3 秒后的任务
  • 第 1 秒:用户打断,shouldStop = true
  • 当前 thinkCycle() 退出
  • 第 3 秒:定时器触发,启动新一轮 thinkCycle()

这会导致打断后 AI 又自己跑起来了,不是用户想要的。

所以打断时必须同时做三件事

  1. 设置 shouldStop = true(让当前执行退出)
  2. clearTimeout(thinkingTimer)(取消已安排的下一次)
  3. 修改状态为 AWAITING_INPUT(让未来的定时器检查不通过)

五、AI主动等待:WAIT 标签的设计

5.1 为什么需要AI主动等待?

人类对话有个基本规则:轮流说话

目前的机制中,AI 思考完一轮,如果没有任何中断,会继续下一轮思考。这意味着 AI 会不停地输出,用户永远没机会插嘴。

WAIT 标签就是为了解决这个问题------让 AI 自己决定"该你说了"。

5.2 使用场景

AI 什么时候应该主动等待?

  • 征求同意:"我发现你的 Downloads 文件夹很乱,要帮你整理一下吗?WAIT"
  • 提问澄清:"你说的'那个文件'指的是哪个?WAIT"
  • 分享发现后等待反馈:"我找到了一个 3 年前的备忘录,好像很有意思,你想看看吗?WAIT"

5.3 实现方式

javascript 复制代码
// 检测回复中是否包含 [WAIT]
if (fullResponse.includes('[WAIT]')) {
  wantsToWait = true;
}

// 在 thinkCycle 末尾决定下一轮状态
if (wantsToWait) {
  state = STATE.AWAITING_INPUT;
  println('[等待] 我先不说了,等你说~', 'gray');
}

设计细节

  • [WAIT] 放在回复的结尾,语义上是"我说完了,该你了"
  • 它只在当前轮次生效,不影响下一轮
  • 检测只是简单的字符串 includes,不需要复杂解析

5.4 让AI学会使用 WAIT

光有代码实现不够,AI 得知道什么时候该用。我们在系统提示词里加了说明:

复制代码
## 探索节奏
当你:
- 想分享一个发现、想法或感受
- 想问用户问题
- 想和用户互动

就在你的发言结尾加上 [WAIT],这会让我停下来等你回复。

## 示例
我觉得这个文件夹很有意思,你想让我继续探索这里吗?[WAIT]

这样 LLM 就会在合适的时机主动输出 [WAIT]


六、工具调用:让AI"动手"做事

6.1 问题:AI 只能输出文字,怎么让它执行操作?

LLM 的本质是文本生成器。给它一段 prompt,它吐出一段文字。

要让 AI "执行操作",我们需要一个约定:AI 输出特定的文字格式,程序识别这个格式后,去执行对应的操作。

这就是工具调用协议

6.2 协议设计

CogitoAgent 的协议非常简单:

复制代码
[TOOL] 工具名称("参数1", "参数2") [/TOOL]

例如:

  • [TOOL] ls("src") [/TOOL] → 列出 src 目录
  • [TOOL] read("README.md") [/TOOL] → 读取 README.md
  • [TOOL] search("人工智能") [/TOOL] → 联网搜索

为什么这么设计?

设计要求 协议如何满足
容易被 LLM 学会 格式简单,类似函数调用,LLM 训练数据中常见
容易被程序解析 正则表达式轻松提取工具名和参数
可读性好 人类看一眼也能理解
不需要特殊 API 任何 LLM 都能输出这种纯文本

6.3 解析过程

javascript 复制代码
// 正则表达式拆解
/\[TOOL\]\s*(\w+)\s*\(([^)]*)\)\s*\[\/TOOL\]/
    │       │      │        │
    │       │      │        └── 结束标记
    │       │      └── 参数部分(括号内)
    │       └── 工具名(字母数字)
    └── 开始标记

为什么支持多个工具调用?

AI 有时需要连续做几件事。比如:先 ls 看看有什么,再 read 读其中一个文件。如果一次响应只支持一个工具调用,AI 就需要输出一次、等程序执行完、再输出第二次。这样效率很低。

支持多个调用后,AI 可以一次输出:

复制代码
[TOOL] ls("src") [/TOOL]
[TOOL] read("src/index.js") [/TOOL]

程序会依次执行,并把所有结果收集起来。

6.4 执行与反馈

执行完工具后,程序需要把结果告诉 AI,这样 AI 才能基于结果做出下一步决策。

javascript 复制代码
addAssistantMessage(fullResponse + `\n\n[工具结果]: ${JSON.stringify(result.data)}`);

添加到历史的消息是这样的:

复制代码
[TOOL] ls("src") [/TOOL]

[工具结果]: {"success":true,"data":["agent/","api/","config.js"]}

AI 看到这段历史,就知道工具执行成功了,并且看到了结果内容。

注意 :我们同时保留了 AI 的原始输出(包含 [TOOL])和工具结果。这样 AI 能理解"我上次说要调用 ls,结果是 XXX"。


七、流式输出的分区展示

7.1 问题:LLM 的响应是"边想边说"

调用 LLM 时,它不是一次性返回完整回复,而是一个 token 一个 token 地"流"回来。

这带来一个 UI 问题:如何区分思考过程正式回复

CogitoAgent 利用了一些模型(如 DeepSeek)提供的 reasoning_content 字段。这个字段专门存放模型的"内部思考",与正式回复 content 分开传输。

7.2 分区策略

我们定义了一个简单的"标签状态机":

复制代码
初始状态
    │
    ▼
收到 reasoning ──→ 打印 "┌─ 思考过程"(灰色),然后打印内容
    │
    ▼
收到 content ──→ 如果思考区开着,先打印 "└──" 关闭思考区
                再打印分隔线和 "▼ 回复内容"(醒目颜色)
                然后打印正文
    │
    ▼
遇到 [TOOL] ──→ 打印灰色小框,缩进展示

7.3 为什么这样设计?

设计决策 原因
思考过程用灰色 用户知道 AI 在想,但不干扰阅读正文
正文用醒目分隔线 明确告诉用户"AI 开始正式回答了"
工具调用用小框缩进 工具是中间过程,不是最终答案,不应该喧宾夺主
思考区自动关闭 如果 AI 没有 reasoning 直接输出 content,不会留下一个空白的思考区

八、把零散的知识串起来

现在我们来看看所有这些机制是如何协同工作的:
Tool LLM 状态机 定时器调度 User Tool LLM 状态机 定时器调度 User #mermaid-svg-ZoDEd1WszTn6qPzR{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-ZoDEd1WszTn6qPzR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZoDEd1WszTn6qPzR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZoDEd1WszTn6qPzR .error-icon{fill:#552222;}#mermaid-svg-ZoDEd1WszTn6qPzR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZoDEd1WszTn6qPzR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZoDEd1WszTn6qPzR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZoDEd1WszTn6qPzR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZoDEd1WszTn6qPzR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZoDEd1WszTn6qPzR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZoDEd1WszTn6qPzR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZoDEd1WszTn6qPzR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZoDEd1WszTn6qPzR .marker.cross{stroke:#333333;}#mermaid-svg-ZoDEd1WszTn6qPzR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZoDEd1WszTn6qPzR p{margin:0;}#mermaid-svg-ZoDEd1WszTn6qPzR .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZoDEd1WszTn6qPzR text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ZoDEd1WszTn6qPzR .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ZoDEd1WszTn6qPzR .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ZoDEd1WszTn6qPzR .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ZoDEd1WszTn6qPzR .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ZoDEd1WszTn6qPzR #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ZoDEd1WszTn6qPzR .sequenceNumber{fill:white;}#mermaid-svg-ZoDEd1WszTn6qPzR #sequencenumber{fill:#333;}#mermaid-svg-ZoDEd1WszTn6qPzR #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ZoDEd1WszTn6qPzR .messageText{fill:#333;stroke:none;}#mermaid-svg-ZoDEd1WszTn6qPzR .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZoDEd1WszTn6qPzR .labelText,#mermaid-svg-ZoDEd1WszTn6qPzR .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ZoDEd1WszTn6qPzR .loopText,#mermaid-svg-ZoDEd1WszTn6qPzR .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ZoDEd1WszTn6qPzR .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-ZoDEd1WszTn6qPzR .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ZoDEd1WszTn6qPzR .noteText,#mermaid-svg-ZoDEd1WszTn6qPzR .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ZoDEd1WszTn6qPzR .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZoDEd1WszTn6qPzR .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZoDEd1WszTn6qPzR .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZoDEd1WszTn6qPzR .actorPopupMenu{position:absolute;}#mermaid-svg-ZoDEd1WszTn6qPzR .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-ZoDEd1WszTn6qPzR .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZoDEd1WszTn6qPzR .actor-man circle,#mermaid-svg-ZoDEd1WszTn6qPzR line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ZoDEd1WszTn6qPzR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 每3秒触发 检查状态 THINKING 调用API 按Enter shouldStop=true 清除定时器 切换到AWAITING_INPUT 中断信号 停止生成 发送消息 切换回THINKING 恢复定时器 继续思考

核心设计哲学

  1. 状态驱动:程序的行为由当前状态决定,而不是散落在各处的条件判断
  2. 中断优先:用户打断是最高的优先级,任何时候都应该被响应
  3. 约定优于配置:工具调用用纯文本标记,不需要复杂的 JSON schema
  4. 透明反馈:AI 的思考、工具执行、最终回答,用户都能看到

九、思考题

学完本章,你可以思考以下问题:

  1. 如果 LLM 的流式响应非常慢(比如 30 秒),用户打断后,我们应该立即切断网络连接吗?为什么?

  2. [WAIT] 用字符串包含检测,如果 AI 在回复中正常讨论 [WAIT] 这个标签本身(比如"你可以用 WAIT 来让我暂停"),会发生什么?如何解决?

  3. 当前的调度间隔是固定的 3 秒。如果某次 thinkCycle() 执行了 5 秒,下一次会在 5 秒后立刻执行(因为定时器是在任务完成后才安排下一次)。这是期望的行为吗?为什么?


十、小结

本章讲解了 CogitoAgent 的心脏------状态机与思考循环:

概念 解决的问题 实现方式
双状态 AI 主动探索 vs 等待用户 THINKING / AWAITING_INPUT
思考循环 如何让程序持续运行不阻塞 递归 setTimeout
用户打断 如何让 AI 立刻停下来 中断标志 + clearTimeout
WAIT 让 AI 主动让出话语权 检测标签 + 状态切换
工具调用 让 AI 执行具体操作 [TOOL] 标记协议
流式分区 区分思考/回复/工具 标签状态机 + ANSI 颜色

下一篇预告:工具系统的设计与实现

我们将深入 tools/ 目录,看看:

  • 文件工具如何安全地读取、写入、复制文件
  • 联网工具如何封装搜索和网页抓取
  • 系统工具如何在 Windows 上管理进程
  • 如何用 4 步添加一个自定义工具

如果这篇文章对你有帮助,欢迎 ⭐Star 支持一下开源项目!

👉 https://gitee.com/cnt-code/cogito-agent 👈

相关推荐
sugar__salt1 小时前
Bun 新一代 JavaScript/TypeScript 运行时:从入门到实战
开发语言·javascript·typescript
无心水1 小时前
【OpenClaw:赚钱】案例19、内容产量5倍、广告收入翻4倍:播客转多平台内容矩阵全自动化实战(OpenAI Whisper + Claude)
java·人工智能·python·ai编程·openclaw·养龙虾·java.time
寻道模式1 小时前
【时间之外】AI不懂Mac吗?
人工智能·macos
挂科边缘1 小时前
手把手教你使用 Faster-Whisper 实时语音输入转文本,本地部署教程
人工智能·语言模型·whisper·faster-whisper·实时语音输入转文本
SUNNY_SHUN1 小时前
把 Whisper、Moonshine、SenseVoice 统统装进手机:sherpa-onnx 离线语音部署框架,GitHub 10.9K Star
人工智能·智能手机·whisper·github
The moon forgets1 小时前
ABot-M0:基于动作流形学习的机器人操作VLA基础模型深度解析
人工智能·pytorch·python·学习·具身智能·vla·点云分割
云烟成雨TD1 小时前
Spring AI 1.x 系列【42】MCP 服务端 Spring Boot 启动器
java·人工智能·spring
有什么事1 小时前
云端虚拟手机:未来移动计算新革命
人工智能·智能手机·云计算
城事漫游Molly2 小时前
AI赋能质性研究(六):跨案例比较分析,5个高质量 Prompt让AI帮你找模式
大数据·人工智能·prompt·ai for science·定性研究