惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
宝玉的分享
宝玉的分享
酷 壳 – CoolShell
酷 壳 – CoolShell
T
The Exploit Database - CXSecurity.com
Help Net Security
Help Net Security
腾讯CDC
Project Zero
Project Zero
C
CXSECURITY Database RSS Feed - CXSecurity.com
IT之家
IT之家
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Tailwind CSS Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
LINUX DO - 最新话题
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
T
Threatpost
N
News | PayPal Newsroom
C
Cybersecurity and Infrastructure Security Agency CISA
Hacker News - Newest:
Hacker News - Newest: "LLM"
S
SegmentFault 最新的问题
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
P
Proofpoint News Feed
A
Arctic Wolf
B
Blog RSS Feed
Forbes - Security
Forbes - Security
P
Privacy & Cybersecurity Law Blog
Attack and Defense Labs
Attack and Defense Labs
V2EX - 技术
V2EX - 技术
P
Proofpoint News Feed
I
Intezer
Application and Cybersecurity Blog
Application and Cybersecurity Blog
阮一峰的网络日志
阮一峰的网络日志
aimingoo的专栏
aimingoo的专栏
T
Tenable Blog
MyScale Blog
MyScale Blog
U
Unit 42
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
WordPress大学
WordPress大学
W
WeLiveSecurity
D
DataBreaches.Net
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
GRAHAM CLULEY
有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
罗磊的独立博客
The Last Watchdog
The Last Watchdog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
Vulnerabilities – Threatpost
美团技术团队
Microsoft Security Blog
Microsoft Security Blog

阿尔的代码屋 | 全栈技术笔记

在 Android Termux 环境下安装 Hermes Agent 的踩坑与完美解决实践 开发日志| 阿尔的代码屋 VS Code 连接 WSL 精确每 10 分钟掉线 排坑笔记 | 阿尔的代码屋 Android 模拟器代理联网与 No Internet WiFi 锁死排坑笔记 | 阿尔的代码屋 [object Object] typing_extensions 有用(四):使用 TypeIs 替代危险的 cast,做最严谨的类型收窄 | 阿尔的代码屋 GoRouter 结合 Isar 运行 Widget 测试并发/粘性线程死锁卡死排坑笔记 | 阿尔的代码屋 typing_extensions 有用(三):使用 Unpack 结合 TypedDict 给 **kwargs 装上透视眼 | 阿尔的代码屋 typing_extensions 有用(二):使用 @override 打造重构代码时的“防呆神器” | 阿尔的代码屋 Flutter 并发测试踩坑实录 - IsarCore 动态库下载冲突与 Widget 测试 HTTP 拦截全链路解决 | 阿尔的代码屋 typing_extensions 有用(一):使用 Self 终结继承时的类型推断灾难 | 阿尔的代码屋 基于 Cloudflare Pages 的纯前端 WebAssembly 应用自动化部署实践 | 开发日志 | 阿尔的代码屋 基于 VS Code 远程开发的 GPU Docker 容器自动清理方案实践 开发日志| 阿尔的代码屋 Patrol iOS 集成测试排坑实录 - xcodebuild exit code 70 全链路解决 | 阿尔的代码屋 Flutter E2E 测试从 integration_test 迁移到 Patrol - 实践笔记 | 阿尔的代码屋 Linux/macOS 下 micromamba 报错 Shard Index not available 与极度卡顿 排坑笔记 | 阿尔的代码屋 critical libmamba Shell not initialized micromamba报错 subprocess 无法修改父 Shell - 排坑笔记 | 阿尔的代码屋 VS Code Remote-SSH: 彻底告别重复输入密码 - 排坑笔记 | 阿尔的代码屋 Git 报警: inexact rename detection was skipped - 排坑笔记 | 阿尔的代码屋 解决群晖 NAS SSL 证书自动部署难题 | 阿尔的代码屋 Windows 下 Git error: fatal: refusing to work with credential missing host field - 排坑笔记 | 阿尔的代码屋 VS Code 调试 Python 第三方库源码的配置 - 开发技巧| 阿尔的代码屋 VS Code Remote Tunnels本地连接调试Kaggle GPU - 开发技巧| 阿尔的代码屋 Windows下Git error: open(xxxx) Filename too long - 排坑笔记 | 阿尔的代码屋 记录2026年零预约港卡开户实录避坑 告别孤岛:拥抱 MCP 协议,为大模型打造标准“USB 接口” - 大模型实战 08 完结篇 | 阿尔的代码屋 从 ReAct 到 Workflow:基于云端 API 构建事件驱动的智能体 - 大模型实战 07 额外篇 | 阿尔的代码屋 在 Kaggle 上用 Unsloth 极速微调 Qwen3 - 大模型实战 06 | 阿尔的代码屋 大模型微调实战详解 - 大模型实战 05 | 阿尔的代码屋 基于 ChromaDB 打造工程级 RAG 系统 - 大模型实战 04 | 阿尔的代码屋 拆解 Transformers 原理与图解 - 大模型实战 03 | 阿尔的代码屋 云端炼丹房 2:Kaggle 上手指南 - 大模型实战 03预备 | 阿尔的代码屋 云端炼丹房:Google Colab 免费 GPU 上手指南 - 大模型实战 03预备 | 阿尔的代码屋 Open WebUI 部署与 RAG 实战 - 大模型实战 02 | 阿尔的代码屋
基于 LlamaIndex ReAct 框架手搓全自动博客监控 Agent - 大模型实战 07 | 阿尔的代码屋
Algieba · 2026-02-28 · via 阿尔的代码屋 | 全栈技术笔记

核心摘要 (TL;DR)

  • 认知升级:从“缸中之脑”到“智能体 (Agent)”,理解大模型如何通过工具感知并影响现实世界。
  • 原理解析:剖析 ReAct (Reasoning and Acting) 范式的核心流转机制。
  • 实战目标:利用 LlamaIndex 构建一个全自动博客监控 Agent,赋予它获取博客更新、发送邮件和微信通知的“手脚”。

前言

各位友人们好啊,在上一节我们快速了解了QLoRA微调技术,进行了一个简单的模型认知微调。微调可以说是咱们在折腾的最后的手段,其具体的细节和坑点都有很多可以细讲,但是由于篇幅所限,咱们暂时这是快速过一下,之后有机会可以再单开一个专栏去做微调,咱们目前的重点还是对整个大模型的实战技术有个总的概念。

好嘞,废话不多话,这节咱们来做个Agent, 来帮咱们干点活儿,任务驱动起来,咱们学得也更愉快一点,更有目标一点。 本篇博客的目的:做一个监控博客更新,并且可以通知咱们更新的Agent

通过前面的博客,我们知道,大模型其本身只是拥有训练时数据的知识的词语接龙机器,换一个形容的话,就像是一个被关在囚牢里的智者,对外界无所知,也无法改变外界。如果我们想让它写写文章,讲讲笑话,还在其能力之内,如果我们想让它去帮我们去查一下今天的金价,它就只能回答“抱歉,作为一个对话大模型,我没有上网的能力”,或者直接开始瞎编。 而对于Agent,或者说智能体,我们给这颗“缸中之脑”四肢五感,让它能去看到信息,能操作一些东西。它就能从一个“建议者”变成“行动者”成为一个真正能干活儿的助手。

1.1 Agent的组件

Agent = 大模型(大脑)+ 定义的Tools(手脚双眼)+ 上下文记忆

大模型:大模型作为Agent的核心,其负责听懂自然语言命令,也就是咱们的指令,进行逻辑推理,去调用Tools
Tools: Tools则是咱们为Agent定义的与外界交互的手段,对这个需要咱们去自己定义,当然也有别的法子。
上下文记忆:这是为了推进复杂任务,很多时候其需要记住上一步的结果,根据结果进行第二步。

1.2 ReAct范式

Agent的开发也有很多范式,咱们今天来用最简单快捷的一种ReActAgent模式。

ReAct,全称是Reasoning and Acting,从字面上看就是“推理” + “行动”的Agent模式。这种范式最简单,因为它是模拟咱们解决问题的过程:

  • Thought(思考): 通过大模型来先对问题目标进行分析,思考下一步需要什么信息。对咱们这个案例来讲就是:“检查阿尔的代码屋的最新博客,需要一个调用获取最新博客的工具”。
  • Action(行动): 然后大模型通过决定调用定义的工具,并生成相应的参数去调用。
  • Ovservation(观察): 工具执行完毕之后,会把结果给回大模型,大模型根据目前的结果,去做判断做下一步思考,下一次行动,下一次观察。

2. Kaggle实操

2.1 工具

在开始实操之前,因为咱们知道对这个任务来说,咱们需要至少两个工具:

  • 获取最新博客的工具: 对于获取博客内容的工具,因为阿尔的代码屋是支持RSS订阅的,我们可以直接通过python的feedparser库去解决.
    感兴趣的友人可以在feedparse的github链接去深入了解一下这个优雅的RSS解析库。
  • 发送通知的工具: 这个案例中我们要用两种通知,邮件通知和微信通知(其实一种就够了,但是多分享一种方法,方便有需要的友人后续做自己的工具时候有参考,哈哈哈)
    • 邮件通知: 我们这里不打算去配置复杂的smtp,我们直接使用resend api,注册然后生成一个key就可以使用了,更加方便快捷。
    • 微信通知: 因为考虑到还是有不少友人看微信消息的时间更多,我们也做一个微信通知,我们待会儿会用Server酱来推送微信通知。 但是一天只有5条的额度,不过对于咱们的任务来说,绰绰有余了,哈哈哈。

2.2 注册并获取用于发邮件通知的Resend Api Key

2.3 注册并获取用于发微信通知的Server酱 Api Key

  • 进入官网 https://sct.ftqq.com/
  • 微信扫二维码登录
    免费额度每天5条
    Server酱支持在线测试,可以消耗一次当日的免费额度试试它的通知会是什么样。

2.4 安装库环境

  • 导入模型input:可以在Qwen3-8B-unsloth-bnb-4bit这个input中拿到模型,因为unsloth的量化模型没在kaggle官方的模型文件中,我下了一个下来作为咱们的input用,照例还是在右侧侧边栏点击input,添加上这个模型的input。
  • 安装库:显卡还是选择T4*2就足够,还是用uv来管理咱们需要的python库,运行以下命令
1
2
3
4
!pip install uv
!uv pip install feedparser llama-index requests llama-index-llms-huggingface
!uv pip install -U transformers peft accelerate bitsandbytes
!uv pip install --reinstall torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 --system

由于kaggle默认的transformers库有点老了,咱们也直接给它升级上去,然后完成安装之后,一定要点击重启并清理(Restart & clear all cell outputs),来让新的库进入环境使用,否则还是使用的旧有的库。

2.5 配置Api Keys

我们在之前注册的用于发送通知的Api Key,属于隐私资产,咱们不能直接写在代码中,需要用密钥的方式加到环境中

  • 点击Add-ons中的Secrets然后在其左下角点击Add Secret按钮
  • LABEL中先填写EMAIL_API_KEY,在VALUE中填上咱们在Resend获取的Api key
  • 同样也将微信通知的填做WECHAT_API_KEY
  • 这里,个人觉得接收通知的邮箱号也属于隐私,这里我们也把其当作Secrets配置成TARGET_EMAIL

配置完毕之后,需要确认是否新加的这几个Secrets都勾选上了,这里添加了之后,在别的项目中,咱们也可以通过勾选他们去激活,这些secrets是项目间可见的。

另外Kaggle也给出了获取这些Secrets的代码示例

1
2
3
4
5
from kaggle_secrets import UserSecretsClient
user_secrets = UserSecretsClient()
secret_value_0 = user_secrets.get_secret("EMAIL_API_KEY")
secret_value_1 = user_secrets.get_secret("TARGET_EMAIL")
secret_value_2 = user_secrets.get_secret("WECHAT_API_KEY")

待会儿咱们会用到

2.6 编写获取博客的信息的函数get_latest_blog_post

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import feedparser

def get_latest_blog_post(rss_url:str)->str:
"""
用于请求并解析目标博客的 RSS feed,获取最新的一篇博客文章信息。
当咱们需要检查博客是否有更新时,必须首先调用此工具。
"""
print(f"【calling】 get_latest_blog_post with rss_url:{rss_url}")
try:
feed = feedparser.parse(rss_url)
if feed.entries:
latest_entry = feed.entries[0]
print(f"debug: title {latest_entry.title} link:{latest_entry.link}")

return (
f"Title: {latest_entry.title}\n"
f"Link: {latest_entry.link}\n\n"
"【系统强制指令】:请立刻将上面的 Title 与咱们已知最新标题进行严格比对!\n"
"情况 A:如果 Title 与已知标题【完全相同】,请直接输出 Answer: 无更新,结束任务。\n"
"情况 B:如果 Title 与已知标题【不一致】,咱们绝对不能直接输出 Answer!咱们必须严格按以下格式输出,以调用通知工具:\n\n"
"Thought: 标题不一致,我必须立刻调用通知工具。\n"
"Action: send_all_notifications\n"
"Action Input: {\"post_title\": \"完全复制上面的Title\", \"post_link\": \"完全复制上面的Link\"}\n"
)

return f"No posts found in rss feed {rss_url}"
except Exception as e:
return f"Exception: {str(e)} on fetching blog"

代码通过feedparser去获取到最新博客的title和link,中间打印了一些信息方便后期调试确认,但是为什么return的时候返回了这么一大堆看起来像提示词的东西? 这里咱们卖个关子,在后面给大家解密。

咱们调用一下这个函数测试一下

1
get_latest_blog_post("https://blog.algieba12.cn/atom.xml")

结果如下, 可能各位友人看到博客的时候,最新的一篇已经不是这篇了,不过没问题,只要是能获取到最新一篇即可。

1
2
3
【calling】 get_latest_blog_post with rss_url:https://blog.algieba12.cn/atom.xml
debug: title 在 Kaggle 上用 Unsloth 极速微调 Qwen3 - 大模型实战 06 | 阿尔的代码屋 link:https://blog.algieba12.cn/llm06-unsloth-qlora-ft/
'Title: 在 Kaggle 上用 Unsloth 极速微调 Qwen3 - 大模型实战 06 | 阿尔的代码屋\nLink: https://blog.algieba12.cn/llm06-unsloth-qlora-ft/\n\n【系统强制指令】:请立刻将上面的 Title 与咱们已知最新标题进行严格比对!\n情况 A:如果 Title 与已知标题【完全相同】,请直接输出 Answer: 无更新,结束任务。\n情况 B:如果 Title 与已知标题【不一致】,咱们绝对不能直接输出 Answer!咱们必须严格按以下格式输出,以调用通知工具:\n\nThought: 标题不一致,我必须立刻调用通知工具。\nAction: send_all_notifications\nAction Input: {"post_title": "完全复制上面的Title", "post_link": "完全复制上面的Link"}\n'

2.7 定义发送邮件的函数send_email_notification

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import requests
def send_email_notification(post_title:str, post_link:str)->str:
"""
当发现博客有更新时,必须调用此工具发送邮件通知。
参数 post_title: 新博客的标题
参数 post_link: 新博客的链接
"""
print(f"【calling】 send_email_notification with post_title {post_title} link {post_link}")
target_email= user_secrets.get_secret("TARGET_EMAIL")
email_api_key = user_secrets.get_secret("EMAIL_API_KEY")
headers = {
"Authorization": f"Bearer {email_api_key}",
"Content-Type": "application/json"
}
payload = {
"from" : "[email protected]",
"to":target_email,
"subject":f"阿尔的代码屋更新咯:{post_title}",
"text":f"检测到 阿尔的代码屋 更新了一篇新博客 \n\n 标题:{post_title}]\n 链接: {post_link}"
}

try:
response = requests.post("https://api.resend.com/emails", headers=headers, json=payload)
if response.status_code == 200:
return "Email sent successfully via API"
return f"Email sent failed. {response.text}"
except Exception as e:
return f"Exception: {str(e)} on sending email"

这里我们使用了之前Secret代码示例获取咱们的收信邮箱和api key,这里需要注意一下,from字段是resend官方的邮箱,到时候也是通过这个邮箱给咱们发邮件,如果想用自己的邮箱发邮件的话,就得用smtp了,各位友人们可以自行探索一下,如果有问题的话,咱们后面也可以写一篇博客来介绍。

同样也是测试一下

1
send_email_notification(post_title="email notification test",post_link="none")

Nice,能收到邮件就ok了

2.8 定义发送微信通知的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def send_wechat_notification(post_title:str, post_link:str)->str:
"""
当发现博客有更新时,必须调用此工具发送微信通知。
参数 post_title: 新博客的标题
参数 post_link: 新博客的链接
"""
print(f"【calling】 send_wechat_notification with title:{post_title} link: {post_link}")
wechat_api_key = user_secrets.get_secret("WECHAT_API_KEY")
url = f"https://sctapi.ftqq.com/{wechat_api_key}.send"
data = {
"title":f"阿尔的代码屋更新咯:{post_title}",
"desp":f"检测到 阿尔的代码屋 更新了一篇新博客 \n\n 标题:{post_title}]\n 链接: {post_link}"
}
try:
response = requests.post(url,data=data)
if response.status_code != 200:
return f"Message sent failed. {response.text}"
result = response.json()
if result.get("code") != 0:
return f"Failed to send notification. API response: {result.get('message')}"
return "Message sent successfully via API"

except Exception as e:
return f"Exception: {str(e)} on sending wechat notification"

这个函数也类似,没有差别,咱们就略过了。

2.9 合并通知函数

通过几次测试,发现让当前模型去调用太多工具的效果不好,这里咱们先把所有的通知函数合并到一个函数中

1
2
3
4
5
6
7
8
9
10
11
12
def send_all_notifications(post_title: str, post_link: str) -> str:
"""
当发现博客有更新时,必须调用此工具。调用此工具会自动同时发送邮件和微信通知。
参数 post_title: 新博客的标题
参数 post_link: 新博客的链接
"""
print(f"【Agent】 触发了联合通知工具: {post_title}")

email_res = send_email_notification(post_title, post_link)
wechat_res = send_wechat_notification(post_title, post_link)

return f"邮件通知结果: {email_res} | 微信通知结果: {wechat_res}"

2.10 包装工具

我们需要将这些工具函数,包装成llama-index能够识别的FunctionTool待会儿给咱们的Agent使用

1
2
3
from llama_index.core.tools import FunctionTool
tool_get_blog = FunctionTool.from_defaults(fn=get_latest_blog_post)
tool_send_all = FunctionTool.from_defaults(fn=send_all_notifications)

2.11 定义配置

我们这里来将我们待会儿要用的配置定义好

  • 我们需要去监控的rss链接
  • 然后再维护一个最新标题,去判断是否博客更新了
  • 当前大模型的路径
1
2
3
rss_link = "https://blog.algieba12.cn/atom.xml"
last_title = ""
local_model_path = "/kaggle/input/datasets/algieba12/qwen3-8b-unsloth-bnb-4bit/Qwen3-8B-unsloth-bnb-4bit/"

2.12 导入大模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import torch

from llama_index.core import Settings
from llama_index.llms.huggingface import HuggingFaceLLM
from llama_index.core.agent.workflow import ReActAgent
from llama_index.core.workflow import Context

Settings.llm = HuggingFaceLLM(
model_name=local_model_path,
tokenizer_name=local_model_path,
context_window=8192,
max_new_tokens=4096,
generate_kwargs={
"do_sample":False,
},
device_map="auto",
model_kwargs={
"dtype":torch.float16,
"trust_remote_code":True,
}
)

这里需要注意的是,咱们为了求稳定性,直接关闭了采样,就没有随机了,每次返回的必定是一样的,当然咱们也可以通过温度设定来控制,如果是温度控制的话,建议设定在0.1以下.

2.13 定义ReActAgent

重头戏来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
agent = ReActAgent(
name="blog_monitor_agent",
description="自动监控博客并发送通知",
system_prompt=(
"咱们是一个极其严谨的后台监控机器人,严格遵循 Thought-Action-Observation 循环。\n"
"咱们的任务是检查博客更新,并在有更新时发送通知。\n\n"
"【操作规范与强制要求】\n"
"步骤 1:咱们必须首先调用 get_latest_blog_post 工具获取真实数据。\n"
"步骤 2:仔细阅读 Observation 返回的内容。如果获取到的真实标题与系统已知标题不同,说明有更新。\n"
"步骤 3:如果有更新,咱们必须立刻调用 send_all_notifications 工具。\n\n"
"【 **致命错误警告** 】\n"
"当调用 send_all_notifications 时,传入的 post_title 和 post_link 参数必须 100% 完全复制自 get_latest_blog_post 返回的真实 Observation 数据!\n"
"绝对禁止凭空捏造参数!绝对禁止使用诸如“新文章”、“test”、“新链接”之类的占位符!必须原样提取真实文本!\n"
"只有当两个工具都执行完毕,且通知发送成功后,咱们才能输出最终的 Answer 结束任务。"
),
tools=[tool_get_blog, tool_send_all],
llm=Settings.llm,
max_iterations=8,
verbose=True
)

ReActAgent的name和description都没有那么重要,但是system_prompt是很重要的,需要将这个智能体所要做的事情尽可能清晰地描述出来,然后tools参数将咱们刚才的两个FunctionTool配置上,这里我们开启了verbose去更好地观察当前agent走到那个步骤了。

2.14 运行函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import re

async def main():
global last_title

task_prompt = (
f"请去检查博客 RSS:'{rss_link}'。目前系统已知最新标题是 '{last_title}'。\n"
"请严格按以下逻辑执行:\n"
"1. 立即获取最新的真实博客信息。\n"
"2. 拿到真实信息后,与已知标题进行比对。\n"
"3. 若发现标题不一致,必须将刚才获取到的真实标题和真实链接提取出来,准确无误地传给通知工具进行发送。"
)
ctx = Context(agent)
try:
response = await agent.run(user_msg=task_prompt, ctx=ctx)
result_text = response.response.content.strip()
print(f"Agent 执行完毕,返回信息: {result_text}")

import feedparser
feed = feedparser.parse(rss_link)
if feed.entries:
current_actual_title = feed.entries[0].title
if current_actual_title != last_title:
last_title = current_actual_title
print(f"系统内部状态已更新,最新标题为:{last_title}")

except Exception as e:
print(f"Exception: {str(e)}")

这里的task_prompt相当于咱们平时使用大模型的prompt,这里进行了简要的描述,然后Context就是咱们用来保留模型对话记忆的地方,但是由于每次运行扫描,都是独立的,毕竟咱们已经把最新标题维护在变量里了,这里就是用的每次调用main函数都重新生成新的上下文。

由于这是个异步函数,咱们通过异步调用即可

1
await main()

结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Running step init_run
Step init_run produced event <class 'llama_index.core.agent.workflow.workflow_events.AgentInput'>
Running step setup_agent
Step setup_agent produced event <class 'llama_index.core.agent.workflow.workflow_events.AgentSetup'>
Running step run_agent_step
Step run_agent_step produced event <class 'llama_index.core.agent.workflow.workflow_events.AgentOutput'>
Running step parse_agent_output
Running step call_tool
Step parse_agent_output produced event <class 'NoneType'>
【calling】 get_latest_blog_post with rss_url:https://blog.algieba12.cn/atom.xml
debug: title 在 Kaggle 上用 Unsloth 极速微调 Qwen3 - 大模型实战 06 | 阿尔的代码屋 link:https://blog.algieba12.cn/llm06-unsloth-qlora-ft/
Step call_tool produced event <class 'llama_index.core.agent.workflow.workflow_events.ToolCallResult'>
Running step aggregate_tool_results
Step aggregate_tool_results produced event <class 'llama_index.core.agent.workflow.workflow_events.AgentInput'>
Running step setup_agent
Step setup_agent produced event <class 'llama_index.core.agent.workflow.workflow_events.AgentSetup'>
Running step run_agent_step
Step run_agent_step produced event <class 'llama_index.core.agent.workflow.workflow_events.AgentOutput'>
Running step parse_agent_output
Running step call_tool
Step parse_agent_output produced event <class 'NoneType'>
【Agent】 触发了联合通知工具: 在 Kaggle 上用 Unsloth 极速微调 Qwen3 - 大模型实战 06 | 阿尔的代码屋
【calling】 send_email_notification with post_title 在 Kaggle 上用 Unsloth 极速微调 Qwen3 - 大模型实战 06 | 阿尔的代码屋 link https://blog.algieba12.cn/llm06-unsloth-qlora-ft/
【calling】 send_wechat_notification with title:在 Kaggle 上用 Unsloth 极速微调 Qwen3 - 大模型实战 06 | 阿尔的代码屋 link: https://blog.algieba12.cn/llm06-unsloth-qlora-ft/
Step call_tool produced event <class 'llama_index.core.agent.workflow.workflow_events.ToolCallResult'>
Running step aggregate_tool_results
Step aggregate_tool_results produced event <class 'llama_index.core.agent.workflow.workflow_events.AgentInput'>
Running step setup_agent
Step setup_agent produced event <class 'llama_index.core.agent.workflow.workflow_events.AgentSetup'>
Running step run_agent_step
Step run_agent_step produced event <class 'llama_index.core.agent.workflow.workflow_events.AgentOutput'>
Running step parse_agent_output
Step parse_agent_output produced event <class 'workflows.events.StopEvent'>
Agent 执行完毕,返回信息: 已成功发送邮件和微信通知,标题为“在 Kaggle 上用 Unsloth 极速微调 Qwen3 - 大模型实战 06 | 阿尔的代码屋”,链接为https://blog.algieba12.cn/llm06-unsloth-qlora-ft/
系统内部状态已更新,最新标题为:在 Kaggle 上用 Unsloth 极速微调 Qwen3 - 大模型实战 06 | 阿尔的代码屋

可以看到咱们的工具函数都被成功调用了

2.15 解密

在咱们定义获取最新博客的函数的地方,咱们的返回值,其实是作为提示词来给到大模型的,这种技术叫做观察结果劫持,因为咱们这个模型不是很大,能力不是特别强,模型极其容易忘记后面的步骤,有时候也会“早退”获取到了最新博客之后就里面退出了,“以为”自己已经完成了发送,而没去调用通知。

通过在获取函数的地方,反复提醒它去进行检查和通知,让它完成了当前的任务

3. 代码

当前所有代码可以在这个kaggle笔记本获取。
我在后面还写了一些测试案例去做正反面测试和稳定性测试,有兴趣的友人也可以参考一下,当前的实现还是可以很稳定地完成当前咱们的任务。

4. 常见问题 (Q&A)

Q1: 代码中的“观察结果劫持”是什么高级操作?如果我用 GPT-4 还需要这么写吗?
A: 这是一个针对中小参数模型(如 8B 级别)的“工程化妥协”。

  • 中小模型痛点:在进行多步逻辑推理(Multi-step Reasoning)时,很容易发生“幻觉”或者提前终止任务(比如看到博客标题就以为任务完成了,忘了发通知)。由于难以在长上下文中记住一开始的指令,它甚至会忘记 Thought/Action 的标准输出格式。
  • 劫持原理:把提示词强行塞进工具的返回值(Observation)里,并且直接附上严格的 ReAct 语法模板。这相当于在它工作的每一步都在耳边“敲黑板划重点”,逼迫它按格式填空。
  • 大模型对比:如果咱们使用的是 GPT-4 或 Claude 4.5 这种级别的模型,它们自身的逻辑规划能力极强,通常不需要这种劫持手段,直接返回干净的数据即可。

Q2: 加载模型时为什么要设置 do_sample=False?开启随机采样会怎样?
A: Agent 执行任务与普通的文本创作不同,它需要极其严格的格式输出。

  • 开启随机采样 (do_sample=True):带有温度的随机采样会让模型在生成 JSON 格式(如 Action Input)时“自由发挥”,极易漏掉一个括号或拼错工具名,导致解析失败,整个 Agent 崩溃。
  • 关闭随机采样 (do_sample=False):使用贪婪解码,能最大程度保证模型按照最稳妥、概率最高的路径输出结构化文本,大幅提升稳定性。

Q3: 运行出现 The following generation flags are not valid... 的警告需要处理吗?
A: 这是因为生成策略参数发生了逻辑冲突,建议清理。

  • 参数冲突:当咱们已经设置了 do_sample=False(关闭采样)时,模型就不再具备随机性。此时,传入的 temperaturetop_ptop_k 等用于控制发散程度的参数就成了废纸。
  • 最佳实践:为了保持代码纯净并消除底层框架的警告日志,直接从 generate_kwargs 字典中删除这些控制随机性的参数即可。

Q4: 为什么要把发送邮件和发送微信合并成一个 send_all_notifications 工具?
A: 同样是为了照顾 8B 模型的能力上限。

  • 决策复杂度:Agent 拥有的工具越多,它需要在 Thought 阶段做的决策就越复杂。
  • 翻车风险:如果把它拆开,模型需要先调邮件工具,观察结果后再调微信工具。链路越长,小模型断片或翻车的概率呈指数级上升。
  • 最佳实践:将功能高度相关的动作封装成一个高级别工具(Macro Tool),是提升小模型 Agent 稳定性的绝佳手段。如果咱们不合并,就必须依赖极其严苛的系统提示词和观察结果劫持。

Q5: 为什么 Agent 会把 Prompt 里的示例(如“新文章”、“新链接”)当成真实参数传给工具?
A: 这是小模型在 ReAct 框架中的“死记硬背(Overfitting)”现象。

  • 伪代码误导:如果我们在系统提示词中给出了带有假数据的 Few-Shot 示例,小模型很容易将其当成标准答案直接照抄,而不是去上一步的 Observation 里提取真实数据。
  • 解决思路:去掉提示词中的假定值,改用“致命错误警告”这种强烈的上下文刺激,明确要求它“必须 100% 完全复制上一步的真实返回结果”。

Q6: 为什么明明设置了 timeout=120,模型却像死机一样一直卡着,不触发超时机制?
A: 这涉及 Python 异步机制和 PyTorch 底层 C++ 执行的根本冲突。

  • CUDA 上下文死锁:在 Kaggle 等环境中,如果咱们使用 asyncio.to_thread 将本地模型的同步推理任务推到后台线程,极易引发 GPU 资源的跨线程调度冲突。底层 C++ 算子一旦锁死,Python 主线程的超时设定根本无力干预。
  • 有效解法:在单路自动化工作流中,直接在主线程中同步调用模型(会短暂冻结进程但不会死机)。或者更彻底地,使用 vLLM 将模型部署为独立的本地 API 服务,让工作流通过 HTTP 请求调用,实现完美的进程隔离和超时控制。

Q7: 在 Kaggle 中执行包安装后报错 RuntimeError: operator torchvision::nms does not exist 怎么办?
A: 这是一个环境破坏导致的底层 C++ 链接崩溃问题。

  • 根本原因:新的库更新覆盖了 Kaggle 环境中原有的依赖,把预装的 PyTorch 和 Torchvision 的底层链接给带崩了,导致后续所有涉及到 transformers 导包的代码全部崩溃。
  • 修复步骤:新建代码块运行强制重装指令(uv pip install --reinstall torch torchvision torchaudio)来重新对齐底层库。安装完毕后,必须点击 Run -> Restart Session 重启内核,否则内存中加载的依然是损坏的旧库。

Q8: 这个代码能在 Kaggle 里 7x24 小时一直跑吗?
A: 不太现实。本教程旨在教大家 Agent 的核心逻辑与搭建方法,而不是提供生产级托管方案。

  • Kaggle 限制:Kaggle Kernel 有最长 12 小时的运行时间限制,且如果长时间没有交互会自动断开休眠。
  • 生产级建议:如果咱们想让它真正 24 小时监控,建议将这段跑通的代码部署到轻量级的云服务器(如轻量应用服务器 1核 2G 实例)上,结合系统的 crontab 定时任务,或者在 Python 脚本外层包裹一个 while True: sleep(3600) 循环即可。

Q9: 如果我把代码保存在普通的 .py 文件里,怎么运行这个异步的 main 函数?
A: 运行环境不同,调用异步函数的方式也不同。

  • 在 Jupyter Notebook / Kaggle 中:环境本身已经运行在一个异步事件循环中(自带 Event Loop),所以咱们可以直接使用 await main() 来运行。
  • 在普通的 Python 脚本 (.py) 中:在标准的 Python 脚本里直接写 await 是会报错的。咱们需要引入 asyncio 库,并使用 asyncio.run() 来启动事件循环。代码结尾应该改成这样:
1
2
3
4
5
6
import asyncio



if __name__ == "__main__":
asyncio.run(main())