AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

Agent 训练与反馈学习

我的学习 →

正在读取学习记录…

训练,是根据样本调整模型内部的数值,让它下次遇到相似输入时更容易做出合适选择。这些可调整的数值叫参数。Agent 的动作选择也可以训练:资料充分时直接回答,资料不足时先检索。

想象带一位新同事处理咨询:你给他看几次“先查资料再答”的完整过程,再指出哪次处理得好。机器把这些反馈变成数字,算出参数该往哪个方向调整。从一个只有两个参数的小模型开始,我们就能实际观察这个过程。

1. 反馈怎样变成训练材料

一段任务的完整操作记录叫轨迹(trajectory):看到了什么、选了什么动作、工具返回了什么、最后有没有完成任务。

时刻观察动作环境返回
1用户问退货期限,当前上下文缺少规则检索退货说明找到“收到商品后 7 天内”
2已取得规则与出处回答用户用户得到期限和依据

反馈可以给整段轨迹打标签,也可以指出某一步应换一个动作。记录“满意”还太粗:回答正确但泄露了个人信息时,应把泄露那一步单独标出来。完成度、隐私和工具使用分别有各自的检查项。

方法训练材料参数更新的方向
SFT,监督微调输入与示范回答,或示范操作轨迹提高示范输出的概率
DPO,直接偏好优化同一个输入下,一份更好的输出与一份较差的输出根据成对偏好调整两份输出的相对概率
RL,强化学习实际行动后得到的奖励根据行动结果提高高回报行为的概率

对“查退款进度”,SFT 可以示范“先查订单”;DPO 可以比较“有依据的进度答复”和“编造到账时间”;RL 则让 Agent 在任务环境中操作,根据是否查到正确订单及是否越权得到反馈。

SFT 文档介绍训练器与数据格式;DPO 论文解释成对偏好目标。下面实现动作选择器的监督学习:两个动作,两个参数,每一步都能算。

2. 从分数到选择概率

给资料充分程度一个数 x:充分为 1,不足为 -1。第二个特征固定为 1,用来学习整体偏好,也叫偏置。模型用参数 w 与 b 计算:

TXT
分数 z = w × x + b
直接回答概率 p = 1 / (1 + e^(-z))
先检索概率 = 1 - p

e 约为 2.718,是数学里的自然常数。这个 sigmoid(S 形函数)把分数压到 0 到 1 之间。z 为 0 时 p 为 0.5;z 越大,直接回答的概率越高。初始参数为零,两种输入的概率都是 0.5。

特征 x人工标签 y希望发生的动作
11直接回答
-10先检索

损失 loss 衡量模型离标签有多远。二分类交叉熵的公式为:

TXT
单条损失 = -[y × ln(p) + (1-y) × ln(1-p)]
平均损失 = 各条损失之和 / 样本数量

ln 表示自然对数。标签为 1 时,只留下 -ln(p):正确动作的概率越高,损失越小。初始 p 为 0.5,损失约为 0.6931。

梯度给出参数变化时损失的变化方向。代码先算 error = p - y,累积 error × 特征,再执行“原参数减去学习率 × 梯度”。学习率决定每次走多大一步。一轮(epoch)表示把训练样本完整用一遍。

特征和人工标签进入预测与损失计算,梯度反馈到参数,留出样本单独评测
图 1:训练数据参与参数更新;留出样本检查更新后的行为。反馈箭头回到参数,评测结果用于选择模型版本。

3. 跑出一次参数更新

在学习目录打开 PowerShell,创建并进入文件夹:

Bash
mkdir training-demo
cd training-demo

保存为 training_demo.py。probability 计算动作概率,train 更新参数,loss 检查学习前后的变化。

Python
"""从人工反馈训练一个两参数动作选择器。"""
import math


def probability(weights, features):
    # 特征一表示资料是否充分,特征二固定为 1,用来学习偏置。
    if len(weights) != 2 or len(features) != 2:
        raise ValueError("权重和特征都需要两个数")
    if not all(type(x) in (int, float) and math.isfinite(x)
               for x in [*weights, *features]):
        raise ValueError("输入需要有限数值")
    score = sum(w * x for w, x in zip(weights, features))
    # 分支写法避免很大的负分数导致指数溢出。
    return 1 / (1 + math.exp(-score)) if score >= 0 else math.exp(score) / (1 + math.exp(score))


def loss(weights, rows):
    # 交叉熵衡量:真实应当选择的动作,获得了多少概率。
    total = 0.0
    for features, label in rows:
        p = min(max(probability(weights, features), 1e-12), 1 - 1e-12)
        total -= label * math.log(p) + (1 - label) * math.log(1 - p)
    return total / len(rows)


def train(rows, epochs=50, rate=0.2):
    if not rows or type(epochs) is not int or epochs <= 0:
        raise ValueError("训练需要样本和正整数轮数")
    if type(rate) not in (int, float) or not math.isfinite(rate) or rate <= 0:
        raise ValueError("学习率需要正数")
    for features, label in rows:
        if type(label) is not int or label not in (0, 1):
            raise ValueError("标签需要整数 0 或 1")
        probability([0.0, 0.0], features)
    weights = [0.0, 0.0]
    for _ in range(epochs):
        gradient = [0.0, 0.0]
        for features, label in rows:
            error = probability(weights, features) - label
            for i in range(2):
                gradient[i] += error * features[i] / len(rows)
        # 按梯度的反方向走一小步,让下一轮损失降低。
        weights = [w - rate * g for w, g in zip(weights, gradient)]
    return weights


def split_groups(records, held_out):
    # 同一任务的不同轨迹必须在同一侧,防止训练答案混入评测。
    return ([value for task, value in records if task not in held_out],
            [value for task, value in records if task in held_out])


def main():
    # 1 表示直接回答;0 表示先检索资料。标签来自预先写好的反馈。
    rows = [([1.0, 1.0], 1), ([-1.0, 1.0], 0)]
    weights = train(rows)
    print(f"训练前损失:{loss([0.0, 0.0], rows):.4f}")
    print(f"训练后损失:{loss(weights, rows):.4f}")
    print(f"资料充分时直接回答概率:{probability(weights, [0.8, 1.0]):.4f}")
    print(f"资料不足时直接回答概率:{probability(weights, [-0.8, 1.0]):.4f}")


if __name__ == "__main__":
    main()

再保存 test_training_demo.py:

测试代码:test_training_demo.py
Python
"""用固定数据检查参数更新、留出验证与错误输入。"""
import unittest
from training_demo import probability, train, loss, split_groups


class TrainingTests(unittest.TestCase):
    def test_zero_weights_give_equal_probability(self):
        self.assertEqual(probability([0.0, 0.0], [1.0, 1.0]), 0.5)

    def test_learning_reduces_loss(self):
        rows = [([1.0, 1.0], 1), ([-1.0, 1.0], 0)]
        weights = train(rows)
        self.assertLess(loss(weights, rows), loss([0.0, 0.0], rows))

    def test_holdout_examples_follow_direction(self):
        weights = train([([1.0, 1.0], 1), ([-1.0, 1.0], 0)])
        self.assertGreater(probability(weights, [0.8, 1.0]), 0.8)
        self.assertLess(probability(weights, [-0.8, 1.0]), 0.2)

    def test_reversing_feedback_reverses_behavior(self):
        weights = train([([1.0, 1.0], 0), ([-1.0, 1.0], 1)])
        self.assertLess(probability(weights, [1.0, 1.0]), 0.2)

    def test_task_groups_stay_separate(self):
        # 同一道任务的多条轨迹一起划分,避免答案泄漏。
        groups = [("a", 1), ("a", 2), ("b", 3)]
        self.assertEqual(split_groups(groups, {"b"}), ([1, 2], [3]))

    def test_empty_training_data(self):
        with self.assertRaises(ValueError):
            train([])

    def test_bad_label(self):
        with self.assertRaises(ValueError):
            train([([1.0, 1.0], 2)])

    def test_nonfinite_feature(self):
        with self.assertRaises(ValueError):
            probability([0.0, 0.0], [float("nan"), 1.0])


if __name__ == "__main__":
    unittest.main()

在同一个窗口检查文件并运行:

Bash
ls
python -X utf8 training_demo.py
python -X utf8 -m unittest -v test_training_demo.py

程序输出:

TXT
训练前损失:0.6931
训练后损失:0.1057
资料充分时直接回答概率:0.8526
资料不足时直接回答概率:0.1474

训练输入是 1 与 -1,最后用 0.8 与 -0.8 检查新的输入。两个参数学出了不同倾向。八项测试还检查标签反转、错误输入与任务分组。

沿 main → train → probability 读:参数从零开始;每轮用现有参数计算概率;与标签比较、积累梯度;一轮结束同时更新两个参数。loss 自身没有修改参数。

4. 改轮数,观察学习程度

在 main 中把 weights = train(rows) 改为 weights = train(rows, epochs=1),运行:

Bash
python -X utf8 training_demo.py

训练后损失变成 0.6444,充分时直接回答概率为 0.5200,不足时为 0.4800。只学一轮,两种动作的倾向刚刚分开。恢复 weights = train(rows),重新运行程序和测试。

再把 main 中两条标签 1、0 对调,程序会向相反方向学习:充分时更倾向检索,不足时更倾向回答。最后把标签恢复原样。标签直接影响训练方向,所以反馈审核首先要确认“这个标签代表我们希望的行为吗”。

5. 从选择器走向 Agent 训练

语言模型有大量参数,输出是一串 token;训练在回答或动作序列上计算损失。Agent 还需要环境:工具、任务状态、允许的操作与完成检查。完整轨迹把奖励与具体操作联系起来。

当前问题优先处理位置检查变化
经常选错工具工具说明、示范轨迹、动作训练相同任务集上的工具选择
回答不错但请求总超时网络与运行时请求耗时、排队和重试
回答格式常常错输出约束、格式样本与校验校验失败种类与占比
只会熟悉题目数据划分、任务覆盖新任务完成率

训练集更新参数;验证集挑选版本与参数;测试集用于最后的独立比较。先按任务、会话或来源分组,再划分。同一问题换几个说法,仍可能包含相同答案。split_groups 按任务 ID 把记录放到同一侧。

强化学习还要设计奖励:只奖励“回复快”,Agent 可能省掉检索;只奖励“用户点赞”,可能更爱迎合。一起观察完成度、权限遵守与资源消耗,保留逐步轨迹以便定位行为。

现象检查位置处理动作
程序提示标签错误rows 的第二项使用整数 0 或 1
程序提示输入错误特征与参数每组两个有限数值
损失下降,新任务反而变差样本与评测划分排查重复任务、来源泄漏和标签偏差
修改后测试失败main 与默认训练函数按改参步骤恢复示例,重跑测试

训练让反馈改变参数;反馈质量决定学习方向;独立评测检查这种方向是否帮助新任务。三个步骤连起来,才能决定采用哪一个版本。

课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.0.1

选择题 · 先判断边界

关于“Agent 训练与反馈学习”,哪种做法更稳妥?

对应知识点:L9-training

选择一个答案
练习 2 / 4 · v1.0.0

简答题 · 解释失败

写出一个失败输入并说明记录字段。

对应知识点:L9-training

练习 3 / 4 · v1.0.0

代码题 · 边界测试

补充正常、边界、失败三组输入和预期结果。

对应知识点:L9-training

练习 4 / 4 · v1.0.0

场景题 · 结果未知

证据不足时如何设计状态和人工接管?

对应知识点:L9-training

图片查看器
100%

文章配图 · 按 Esc 关闭