【RL】advantages 与 ratio之间的关系

好的,我们来梳理一下整个流程,澄清 ratioadvantages 的关系以及各自的作用。

核心思想:advantagesratio 是两个独立计算的、服务于同一个最终目标的组件。

  • advantages (优势) 回答了问题:"在某个状态下,采取某个动作,得到的结果比平均水平好多少? " 它的作用是提供学习信号的方向和强度

    • advantages > 0:这是个好动作,应该增加它的概率。
    • advantages < 0:这是个坏动作,应该降低它的概率。
    • advantages绝对值越大,这个学习信号就越强。
  • ratio (重要性采样比率) 回答了问题:"我的新策略相比于产生这些数据的旧策略,在多大程度上改变了这个动作的概率? " 它的作用是修正因为策略变化而带来的分布不匹配

它们在PPO损失函数中被结合在一起,但它们的计算过程是完全分开的。


1. advantages 是如何计算的?(回顾)

advantages 的计算发生在 ratio 计算之前 ,并且完全不依赖于 ratio 。根据我们之前的分析(grpo模式),它的计算流程如下:

  1. 获取序列级奖励 R_i:从Reward Model得到每个完整序列的得分。
  2. 组内奖励归一化 :在同一prompt生成的N个序列组成的组内,对 R_i 进行标准化 (R_i - group_mean) / group_std。这个结果就是序列级优势 Â_i
  3. 扩展与传播 :将 Â_i 放在序列末尾,然后通过 compute_reinforce_return 以折扣累积的方式向前传播,得到每个token的优势 advantages 矩阵。

关键点:到这一步为止,我们只用到了奖励(Reward)和折扣因子(gamma),完全没有用到新旧策略的概率。


2. ratio 计算后干什么用的?

ratio 计算出来后,它的唯一作用就是作为PPO裁剪替代目标函数中的一个核心乘数,来**调制(modulate)**已经计算好的advantages

让我们来看 ActorWorker.loss_func 的核心计算:

python 复制代码
# 1. advantages 已经作为输入 data.batch["advantages"] 准备好了
advantages = data.batch["advantages"]

# 2. 在这里计算 ratio
ratio = (log_probs - old_log_probs).exp()

# 3. ratio 和 advantages 在这里第一次"相遇"并结合
#    它们共同构成了PPO的两个替代目标 (surrogate objectives)
surr1 = ratio * advantages
surr2 = ratio.clamp(1 - pg_clip, 1 + pg_clip) * advantages

# 4. 计算最终的PPO损失
loss = -torch.min(surr1, surr2)

ratio 的作用可以这样理解:

  • 假设我们有一个token,它的advantages+2.0(一个非常好的信号)。

    • 如果 ratio1.5,意味着新策略已经将这个好动作的概率提高了50%。surr1 将会是 1.5 * 2.0 = 3.0,这是一个很强的正向梯度信号,鼓励策略继续提高概率。
    • 如果 ratio0.7,意味着新策略反而降低了这个好动作的概率。surr1 将是 0.7 * 2.0 = 1.4。虽然梯度信号仍然是正向的(因为优势是正的),但它的强度被ratio削弱了。这会驱动模型"纠正错误",转而提高这个动作的概率。
  • 假设另一个token的advantages-1.0(一个坏信号)。

    • 如果 ratio0.6,意味着新策略已经成功地将这个坏动作的概率降低了40%。surr10.6 * (-1.0) = -0.6。损失 -surr1 就是 0.6,一个正的损失值。
    • 如果 ratio1.3,意味着新策略错误地提高了这个坏动作的概率。surr11.3 * (-1.0) = -1.3。损失 -surr1 就是 1.3,一个更大的损失值,会产生更强的梯度来惩罚这个错误的方向。

PPO的裁剪机制(surr2 进一步限制了ratio的作用。如果ratio变得过大或过小,它会被裁剪到一个固定的范围内,防止单次更新对策略的改变过大,从而保证了训练的稳定性。


总结:清晰的流程图

为了彻底理清思路,我们可以画出这样一个计算流程:

复制代码
+---------------------------+       +---------------------------------+
|     Reward Model          |       |      Policy Model (Actor)       |
+---------------------------+       +---------------------------------+
            |                               |           |
            v                               |           v
+---------------------------+               |  +---------------------------+
|  `response_level_rewards` |               |  |  `logits` (from new policy) |
+---------------------------+               |  +---------------------------+
            |                               |                 |
            v                               |                 v
+---------------------------+               |  +--------------------------------+
|  `reward_postprocess`     |               |  |  `op_compute_log_probs`        |
|  (Group Normalization)    |               |  +--------------------------------+
+---------------------------+               |                 |
            |                               |                 v
            v                               |  +--------------------------------+
|  `expand_to_token_level`  |               |  | `log_probs` (new policy logp)  |
+---------------------------+               |  +--------------------------------+
            |                               |
            v                               |
+---------------------------+               |
| `compute_reinforce_return`|               |
+---------------------------+               |
            |                               |
            v                               |
+---------------------------+               |
|  **advantages**           |               |
|   (Shape: bs, seq_len)    |               |
+---------------------------+               |
            |                               |
            |                               |       +--------------------------------+
            |                               +------>| `old_log_probs` (from memory)|
            |                                       +--------------------------------+
            |                                                     |
            |   +-------------------------------------------------+
            |   |
            v   v
+---------------------------+
| `ratio` = exp(log_probs - |
|            old_log_probs) |
+---------------------------+
            |
            v
+---------------------------+
|  PPO Loss Calculation     |
| `loss = -min(r*A, c(r)*A)`|
+---------------------------+
            |
            v
+---------------------------+
|   Final Scalar Loss       |
|    (for backprop)         |
+---------------------------+

结论:
ratioadvantages是PPO算法中两个正交(Orthogonal)的组件。advantages由奖励信号计算而来,提供了**"学什么"(方向和强度)的信息。ratio由新旧策略的概率计算而来,提供了"学了多少"**(策略变化程度)的信息,并用于修正学习过程。它们在最终的PPPO损失函数中才被结合起来,共同决定了最终的梯度更新。

您提的问题非常好,这正是理解策略梯度方法中损失函数符号的关键!我们来彻底澄清一下。

核心原则:梯度下降与目标最大化

  1. 机器学习的通用框架 :我们通常使用梯度下降(Gradient Descent)来优化模型。梯度下降的目的是 最小化(minimize)一个损失函数(Loss Function)
  2. 强化学习的目标 :在强化学习(包括PPO)中,我们的目标是最大化(maximize)一个期望回报目标函数(Objective Function) ,我们用 J(θ) 表示。我们希望找到一组模型参数 θ,使得 J(θ) 最大。

这两个目标是相反的!一个是最小化,一个是最大化。如何统一呢?

解决方案 :将我们的损失函数(Loss)定义为目标函数(Objective)的相反数
Loss(θ) = -J(θ)

这样一来,最小化Loss(θ) 就等价于 最大化J(θ)


应用到PPO

在PPO中,我们的目标函数 J(θ) 是:
J(θ) = E [ min( ratio * A, clip(ratio) * A ) ]

我们希望最大化这个 J(θ)。因此,我们定义的损失函数 pg_loss 就是:
pg_loss = - E [ min( ratio * A, clip(ratio) * A ) ]

或者在代码中,对于单个样本的token:
loss = -torch.min(surr1, surr2)

现在,我们来分析 pg_loss 的符号应该是正还是负。

pg_loss 应该是正还是负?

答案是:一个健康的、正在学习的模型的 pg_loss 通常应该是负的。

我们来分析两种情况:

情况1:好的动作 (advantages > 0)
  • 我们希望模型增加 这个好动作的概率,也就是让 ratio > 1
  • 此时,ratio * A 是一个正数
  • clip(ratio) * A 也是一个正数
  • 因此,min(surr1, surr2) 是一个正数
  • 那么,loss = -min(...) 就是一个负数

直观理解 :当模型做出正确的行为(采取了带来正优势的动作)时,我们不应该"惩罚"它。但梯度下降只能最小化损失。怎么办?我们给它一个负的损失 。对于梯度下降来说,最小化一个负数意味着让它变得更负(例如,从-0.1变到-0.5)。这个"让损失更负"的梯度方向,恰好就是"增加好动作概率"的正确方向。

情况2:坏的动作 (advantages < 0)
  • 我们希望模型减小 这个坏动作的概率,也就是让 ratio < 1
  • 此时,ratio * A 是一个正数 (负数 * 负数 = 正数,假设 ratio 也为负,但实际上ratio恒为正)。让我们更严谨一点:
    • advantages 是负数。
    • ratio 总是正数。
    • 所以 ratio * A 是一个负数
    • clip(ratio) * A 也是一个负数
  • 因此,min(surr1, surr2) 是一个负数
  • 那么,loss = -min(...) 就是一个正数

直观理解 :当模型做出错误的行为(采取了带来负优势的动作)时,我们应该惩罚它 。一个正的损失正好扮演了这个"惩罚"的角色。梯度下降会努力去最小化这个正的损失,而最小化它的方向,恰好就是"减小坏动作概率"的正确方向。


回到您的例子:surr1 = 1.3 * (-1.0) = -1.3

这个例子属于情况2 的变种,是一个特别需要被惩罚的情况。

  • advantages = -1.0 : 这是一个坏动作
  • ratio = 1.3 : 模型不但没有降低这个坏动作的概率,反而错误地增加了它
  • min(surr1, surr2) : 此时 surr1-1.3surr2(裁剪后的)也会是一个负数。所以 min(...) 是一个负数
  • loss = -min(...) = 1.3 : 我们得到了一个正的损失值 1.3

这个正的损失 1.3 意味着什么?

它意味着一个惩罚信号 。梯度下降算法看到一个正的损失,就会产生梯度来减小它。如何减小这个损失呢?那就是要减小 ratio 。减小ratio就意味着降低当前策略 π_θ 采取这个坏动作的概率。

"一个更大的损失值,会产生更强的梯度来惩罚这个错误的方向" 这句话的含义是:

  • 假设在另一个场景中,ratio=1.1, advantages=-1.0,那么 loss 会是 1.1
  • 我们的场景中,ratio=1.3, advantages=-1.0loss1.3
  • 因为 1.3 > 1.1,所以当前场景的惩罚更重 ,产生的梯度也更强,会更有力地"纠正"模型,让它不要再增加这个坏动作的概率。

总结

  • pg_loss 的符号是有意义的

    • 负损失 ≈ 奖励信号(Reward Signal),鼓励当前方向。
    • 正损失 ≈ 惩罚信号(Penalty Signal),抑制当前方向。
  • 一个合理的 pg_loss 曲线

    • 在整个批次中,通常好的动作比坏的动作多(或者说,我们希望模型能更多地做出好动作)。
    • 因此,批次中所有token的损失加权平均后,总的 pg_loss 倾向于是负的
    • 随着训练的进行,模型越来越好,能获得更高的优势,所以 pg_loss 会变得越来越负

这完美地解释了您在第一张健康的 pg_loss 图中看到的现象:曲线是负的,并且有下降(变得更负)的趋势。

好的,我们来通过一个完整的、具体的例子,从advantages的计算一直到最终loss的形状,来详细说明这个过程。

我们将设定一个batch_size=2seq_len=5pg_clip=0.2 的小场景。


1. advantages 的计算(GRPO/REINFORCE 风格)

假设我们已经经过了奖励模型和奖励归一化,得到了一个稀疏的token_level_rewards张量。这个张量在序列的最后非填充位置有一个值(即序列级优势Â_i),其他地方都是0。我们还假设折扣因子gamma=0.99

输入:token_level_rewards
  • 样本1 : 长度为4,序列级优势 Â_1 = 1.5
  • 样本2 : 长度为5,序列级优势 Â_2 = -0.8
python 复制代码
import torch

token_level_rewards = torch.tensor([
    #  t=0,  t=1,  t=2,  t=3,  t=4(pad)
    [ 0.0,  0.0,  0.0,  1.5,  0.0],  # 样本 1
    [ 0.0,  0.0,  0.0,  0.0, -0.8]   # 样本 2
])
计算过程:compute_reinforce_return

这个函数从后向前遍历序列,计算每个时间步的未来折扣奖励总和。

  • 对于样本 1 (Â_1 = 1.5, T=3):

    • adv[t=4](pad) = 0
    • adv[t=3] = reward[3] + gamma * adv[4] = 1.5 + 0.99 * 0 = 1.5
    • adv[t=2] = reward[2] + gamma * adv[3] = 0.0 + 0.99 * 1.5 = 1.485
    • adv[t=1] = reward[1] + gamma * adv[2] = 0.0 + 0.99 * 1.485 = 1.470
    • adv[t=0] = reward[0] + gamma * adv[1] = 0.0 + 0.99 * 1.470 = 1.455
  • 对于样本 2 (Â_2 = -0.8, T=4):

    • adv[t=4] = reward[4] + gamma * 0 = -0.8 + 0 = -0.8
    • adv[t=3] = reward[3] + gamma * adv[4] = 0.0 + 0.99 * (-0.8) = -0.792
    • adv[t=2] = reward[2] + gamma * adv[3] = 0.0 + 0.99 * (-0.792) = -0.784
    • adv[t=1] = reward[1] + gamma * adv[2] = 0.0 + 0.99 * (-0.784) = -0.776
    • adv[t=0] = reward[0] + gamma * adv[1] = 0.0 + 0.99 * (-0.776) = -0.768
输出:advantages 矩阵
python 复制代码
advantages = torch.tensor([
    [1.455, 1.470, 1.485,  1.500,  0.000],  # 样本 1, 注意最后一个是padding, adv应为0
    [-0.768, -0.776, -0.784, -0.792, -0.800]  # 样本 2
])

# advantages 形状: torch.Size([2, 5])

(注意: 实际代码中会用mask将padding位置的advantage清零)


2. loss 的计算

现在我们有了advantages,还需要一个ratio矩阵。我们假设已经计算好了ratio

输入
  • advantages 矩阵 (如上)

  • ratio 矩阵 (假设值):

    python 复制代码
    ratio = torch.tensor([
        [1.10, 1.25, 0.90, 1.05, 1.00],  # 样本 1
        [0.70, 0.95, 1.15, 1.30, 0.85]   # 样本 2
    ])
  • pg_clip = 0.2,所以裁剪区间是 [0.8, 1.2]

计算过程
a) 计算 surr1 = ratio * advantages

这是逐元素的乘法。

python 复制代码
surr1 = ratio * advantages

# surr1 的值:
# tensor([
#   [1.10*1.455, 1.25*1.470, 0.90*1.485, 1.05*1.500, 1.00*0.0],
#   [0.70*(-0.768), 0.95*(-0.776), 1.15*(-0.784), 1.30*(-0.792), 0.85*(-0.800)]
# ])
#
# tensor([
#   [ 1.600,  1.838,  1.337,  1.575,  0.000],
#   [-0.538, -0.737, -0.902, -1.030, -0.680]
# ])
b) 计算 surr2 = ratio.clamp(...) * advantages
  1. 首先裁剪 ratio:

    python 复制代码
    ratio_clamped = ratio.clamp(0.8, 1.2)
    # tensor([
    #   [1.10, 1.20, 0.90, 1.05, 1.00],  # 1.25被裁剪到1.2
    #   [0.80, 0.95, 1.15, 1.20, 0.85]   # 0.70被裁剪到0.8, 1.30被裁剪到1.2
    # ])
  2. 然后与 advantages 相乘:

    python 复制代码
    surr2 = ratio_clamped * advantages
    # tensor([
    #   [1.10*1.455, 1.20*1.470, 0.90*1.485, 1.05*1.500, 1.00*0.0],
    #   [0.80*(-0.768), 0.95*(-0.776), 1.15*(-0.784), 1.20*(-0.792), 0.85*(-0.800)]
    # ])
    #
    # tensor([
    #   [ 1.600,  1.764,  1.337,  1.575,  0.000],
    #   [-0.614, -0.737, -0.902, -0.950, -0.680]
    # ])
c) 计算 torch.min(surr1, surr2)

surr1surr2 逐元素取最小值。

  • advantages > 0 时 (样本1) : 我们希望最大化目标,所以取 min(r*A, clip(r)*A)。当r > 1+ε时,clip(r)*A会更小,起到限制作用。
  • advantages < 0 时 (样本2) : 我们希望最小化目标(因为A是负的),所以取 max(r*A, clip(r)*A)。PPO论文中的目标是 min,但当A<0时,min(r*A, c(r)*A) 等价于 max(r, c(r)) * A。这里我们直接按 min 计算。
python 复制代码
#   surr1:
#   [ 1.600,  1.838,  1.337,  1.575,  0.000]
#   [-0.538, -0.737, -0.902, -1.030, -0.680]
#
#   surr2:
#   [ 1.600,  1.764,  1.337,  1.575,  0.000]
#   [-0.614, -0.737, -0.902, -0.950, -0.680]

min_surr = torch.min(surr1, surr2)
# tensor([
#   [ 1.600,  1.764,  1.337,  1.575,  0.000],  # 1.838 > 1.764,所以取 1.764
#   [-0.614, -0.737, -0.902, -1.030, -0.680]  # -0.538 > -0.614, 所以取 -0.614; -1.030 < -0.950, 所以取 -1.030
# ])
d) 计算 loss = -torch.min(surr1, surr2)

最后,取上一步结果的相反数。

python 复制代码
loss = -min_surr
# tensor([
#   [-1.600, -1.764, -1.337, -1.575, -0.000],
#   [ 0.614,  0.737,  0.902,  1.030,  0.680]
# ])

最终输出:loss 矩阵和形状

最终得到的 loss 是一个与 advantagesratio 形状完全相同的矩阵。

  • loss 矩阵 :

    复制代码
    tensor([
      [-1.600, -1.764, -1.337, -1.575, -0.000],
      [ 0.614,  0.737,  0.902,  1.030,  0.680]
    ])
  • loss 形状 : torch.Size([2, 5]) (即 (batch_size, sequence_length))

这个 loss 矩阵随后会被传入 agg_loss 函数,与 mask 一起被聚合成一个单一的标量值,用于执行反向传播。

观察结果:

  • 对于优势为正的样本1,其所有token的损失都是负的(奖励信号)。
  • 对于优势为负的样本2,其所有token的损失都是正的 (惩罚信号)。
    这与我们之前对pg_loss符号的理论分析完全一致。

您提的这个问题非常敏锐,正指出了PPO损失函数在advantages < 0时的一个微妙但关键的数学变换。这确实是初学者容易困惑的地方。我们来彻底把它讲清楚。

PPO的目标:防止策略变得太差

首先,我们回顾一下PPO裁剪目标的直观思想:

  • advantages > 0(好动作)时:我们想增加 ratio 来提高这个动作的概率,但又不希望 ratio 变得太大 ,以免策略更新过猛。所以我们用 min(ratio * A, (1+ε) * A) 来给它设一个上限。
  • advantages < 0(坏动作)时:我们想减小 ratio 来降低这个动作的概率,但又不希望 ratio 变得太小,以免策略更新过猛。所以我们应该给它设一个下限。

数学推导:为什么 minA<0 时能起到"设下限"的作用

让我们来分析 min(r*A, c(r)*A)A < 0 时的行为。

这里的 rratioc(r)clip(ratio, 1-ε, 1+ε)

我们知道,对于任意两个数 xy,以及一个负数 A
min(x*A, y*A) = max(x, y) * A

为什么?

因为乘以一个负数会颠倒大小关系。例如,3 > 2,但 3 * (-5) < 2 * (-5),即 -15 < -10。所以,原来较大的数乘以负数后反而变小了。因此,取两个负积的最小值,等价于取原来两个正数中的最大值,再乘以这个负数。

把这个性质应用到PPO中:
min(r * A, c(r) * A) = max(r, c(r)) * A (因为 A < 0)

现在我们来分析 max(r, c(r))

  • c(r) 的值域是 [1-ε, 1+ε]
  • rratio,可以是任意正数。

分两种情况讨论 r 的值:

  1. 如果 r[1-ε, 1+ε] 区间内 : 此时 r 没有被裁剪,c(r) = r。那么 max(r, c(r)) = r。目标函数简化为 r * A。这很合理,因为策略变化不大,我们直接使用标准的策略梯度项。

  2. 如果 r 超出 [1-ε, 1+ε] 区间:

    • Case A: r > 1+ε (策略错误地增加了坏动作的概率) :
      • c(r) = 1+ε
      • max(r, c(r)) = max(r, 1+ε) = r
      • 目标函数仍然是 r * A
    • Case B: r < 1-ε (策略正确地降低了坏动作的概率,但可能降得太多) :
      • c(r) = 1-ε
      • max(r, c(r)) = max(r, 1-ε) = 1-ε
      • 目标函数变为 (1-ε) * A

关键洞察

让我们把两种情况的目标函数放在一起看:
J_clipped(θ) = E [ ... ]

其中 [...] 部分是:

  • A > 0: min(r*A, (1+ε)*A)
  • A < 0: max(r*A, (1-ε)*A) (这是从上面推导出来的等价形式)

这两个式子可以统一写成一个 if-else 结构,这在很多PPO的实现中可以看到,例如 Stable Baselines3。

我们现在回到 loss = -torch.min(surr1, surr2) 这种看似简单的实现。 它为什么是对的?

因为它利用了我们最开始推导的数学性质 min(x*A, y*A) = max(x, y) * A

让我们回到您的例子:A = -1.0 (负数),r = 1.3ε = 0.2

  • r*A = 1.3 * (-1.0) = -1.3
  • c(r) = clip(1.3, 0.8, 1.2) = 1.2
  • c(r)*A = 1.2 * (-1.0) = -1.2
  • torch.min(-1.3, -1.2) 的结果是 -1.3

这与我们推导的 max(r, c(r)) * A 的结果是否一致?

  • max(r, c(r)) = max(1.3, 1.2) = 1.3
  • max(r, c(r)) * A = 1.3 * (-1.0) = -1.3
    结果完全一致!

再看一个例子:A = -1.0 (负数),r = 0.7ε = 0.2

  • r*A = 0.7 * (-1.0) = -0.7
  • c(r) = clip(0.7, 0.8, 1.2) = 0.8
  • c(r)*A = 0.8 * (-1.0) = -0.8
  • torch.min(-0.7, -0.8) 的结果是 -0.8

这与我们推导的 max(r, c(r)) * A 的结果是否一致?

  • max(r, c(r)) = max(0.7, 0.8) = 0.8
  • max(r, c(r)) * A = 0.8 * (-1.0) = -0.8
    结果再次完全一致!

结论与直观解释

PPO论文中那个统一的 min 公式是一个非常巧妙的数学构造。它通过一个简单的 min 操作,同时实现了两种情况下的目标:

  • A > 0:

    • min(r*A, (1+ε)*A)
    • 它实际上是在惩罚 r 过大 (r > 1+ε) 的情况,通过选择更小的 (1+ε)*A限制梯度的上限
  • A < 0:

    • min(r*A, (1-ε)*A)
    • 它实际上是在惩罚 r 过小 (r < 1-ε) 的情况,通过选择更小的 r*A (因为 r*A(1-ε)*A更负,所以更小)来限制梯度的下限。等一下,这里的逻辑反了。
    • 让我们重新审视 A<0 的情况。我们希望 最大化 J(θ)。因为 A 是负数,所以 r*Ac(r)*A 都是负数。要最大化一个负数,就要让它的绝对值变得更小(更接近0)。
    • min(r*A, c(r)*A) 选出的是更负的那个值,也就是绝对值更大的那个。
    • 这意味着,当 r 偏离 c(r) 时,PPO会选择一个更差 的目标值(更负的目标值),从而产生一个更大的正损失 (loss = -J(θ)) 来惩罚这种偏离。
    • 例子 : r=0.7, A=-1.0. r*A=-0.7, c(r)*A=-0.8. min 选了-0.8。而r*A(未裁剪的目标)是-0.7。因为 -0.8 < -0.7,所以PPO选择了一个更低的目标值,这会导致更大的惩罚。这正好起到了限制r不要变得过小的作用。

最终总结 :

PPO的 min(...) 公式是一个简洁而强大的统一形式。它在 A>0A<0 时,通过与 advantages 相乘的符号翻转效应,都巧妙地实现了对 ratio 偏离 [1-ε, 1+ε] 区间的惩罚,从而将策略更新限制在一个可信的范围内。尽管 A<0 时的推导看起来不那么直观,但数学上是完全成立的,并且代码中直接使用 torch.min 就是正确的实现方式。

相关推荐
NAGNIP13 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab14 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab14 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
AngelPP18 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年18 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼18 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS18 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区19 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈19 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang20 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx