临时

第7章:Agent 层 — Main Crew

本章目标:深入理解 CrewAI 框架下的主 Agent 设计,包括 Agent/Task YAML 配置、Prompt 工程、ReAct 推理循环、step_callback 详细模式,以及工厂模式的应用。
本章依赖:第3章(数据模型)、第5章(Runner)、第6章(Session)
对应源码:
  • [xiaopaw/agents/main_crew.py](file:///d:/ProjectsCodes/企业级智能体实战/xiaopow/xiaopaw/agents/main_crew.py)
  • [xiaopaw/agents/models.py](file:///d:/ProjectsCodes/企业级智能体实战/xiaopow/xiaopaw/agents/models.py)
  • [xiaopaw/agents/config/agents.yaml](file:///d:/ProjectsCodes/企业级智能体实战/xiaopow/xiaopaw/agents/config/agents.yaml)
  • [xiaopaw/agents/config/tasks.yaml](file:///d:/ProjectsCodes/企业级智能体实战/xiaopow/xiaopaw/agents/config/tasks.yaml)
  • [xiaopaw/tools/intermediate_tool.py](file:///d:/ProjectsCodes/企业级智能体实战/xiaopow/xiaopaw/tools/intermediate_tool.py)
预计时间:2-3 小时

7.1 什么是 Agent?通俗理解

在聊代码之前,先理解一个概念:Agent(智能体)到底是什么?
想象你有一个非常聪明的助手,你给他一个任务,他:
  1. 思考:这个任务我应该怎么做?
  1. 行动:调用工具去完成(查资料、写代码、发消息)
  1. 观察:看看工具返回了什么结果
  1. 判断:结果对吗?需要继续吗?还是可以给出最终答案了?
  1. 回到步骤 1(如果需要的话)
这就是 Agent 的核心工作方式——ReAct(推理-行动)循环

7.2 CrewAI 框架简介

CrewAI 是一个 Python 框架,帮助我们快速构建 AI Agent。它提供了 Agent、Task、Crew 三个核心概念。

7.2.1 核心概念一览

概念
通俗理解
在代码中
Agent
"一个员工" — 有身份、有目标、有技能
Agent(role=..., goal=..., tools=...)
Task
"一个任务" — 具体要做的事情
Task(description=..., expected_output=...)
Tool
"一个工具" — Agent 可以调用的能力
SkillLoaderTool(), IntermediateTool()
Crew
"一个团队" — 包含 Agent + Task
Crew(agents=[...], tasks=[...])

7.2.2 CrewAI 的执行流程


7.3 XiaoPaw 的 Agent 架构

7.3.1 整体设计

7.3.2 设计原则

原则一:极简主 Agent(单工具原则)
主 Agent 只有一个能力扩展工具:SkillLoaderTool。所有专业能力(PDF 处理、飞书操作、网络搜索等)都通过 Skill 生态接入。这样做的好处:
  • 主 Agent 的 Prompt 更短(不需要列举所有工具)
  • 新增 Skill 不需要修改主 Agent
  • 主 Agent 的职责清晰:理解意图 → 编排 Skill
原则二:结构化输出
使用 Pydantic 模型强制 LLM 输出 JSON 格式:
为什么需要结构化输出?
  • 程序可以可靠地解析 Agent 的回复(result.pydantic.reply
  • 可以追踪 Agent 用了哪些 Skill(result.pydantic.used_skills
  • 避免 LLM 输出格式不一致导致的解析错误
原则三:YAML + Python 分离
配置层
位置
内容
修改频率
YAML(人设层)
agents.yaml, tasks.yaml
Agent 的角色、目标、行为规范
经常调整(调 Prompt)
Python(代码层)
main_crew.py
工具绑定、LLM 绑定、工厂函数
较少修改(代码稳定)
原则四:工厂模式防污染
每次请求创建新的 Crew 实例,防止上一轮的状态污染下一轮。详见 7.7 节。

7.4 Agent 配置详解(agents.yaml)

7.4.1 完整配置

7.4.2 配置字段详解

字段
作用
对 LLM 的影响
示例
role
定义身份/职位
影响回复风格和专业程度
"XiaoPaw 工作助手"
goal
定义最终目标
指导整个推理方向
"通过 Skills 完成任务"
backstory
背景故事 + 行为规范
最重要的 Prompt 工程部分
工作流程、边界、策略
max_iter
最大迭代次数
安全阀,防止无限循环
50

7.4.3 backstory 中的 Prompt 工程技巧

技巧一:明确工作流程(Step-by-Step)
为什么 Step-by-Step 有效?因为 LLM 是"逐 token 生成"的,明确的步骤让它更容易产生正确的推理链。
技巧二:明确行为边界(负面约束)
负面约束("不要做什么")和正面约束("要做什么")同样重要。LLM 有时会"过度自信"编造结果,明确禁止可以防止。
技巧三:搜索策略优先级
让 Agent 知道工具之间的优先级关系,避免"大材小用"。

7.5 Task 配置详解(tasks.yaml)

7.5.1 完整配置

7.5.2 Task 的两要素

要素
作用
示例
description
任务的"输入":告诉 Agent 当前要处理什么
历史对话 + 用户消息
expected_output
任务的"输出格式":告诉 Agent 最终要返回什么
JSON 格式 {reply, used_skills}

7.5.3 模板变量注入

description 中的 {history}{user_message}模板变量,在运行时通过 akickoff(inputs={...}) 注入:
注入过程

7.5.4 结构化输出(output_pydantic)

在 Task 中使用
使用结果

7.6 历史格式化(_format_history)

7.6.1 完整实现

7.6.2 输出示例

完整历史只有 4 条时(不截断)
完整历史有 30 条,截断到 20 条时

7.6.3 设计要点

  1. 不直接丢弃历史:超出部分不是直接扔掉,而是告知 Agent "存在更早的历史"
  1. 引导 Agent 使用 history_reader:如果 Agent 需要更早的上下文,会主动调用 history_reader Skill
  1. 不暴露时间戳:只展示对话内容,不展示原始时间戳(保持简洁)
  1. 中文角色名:比英文更自然,LLM 处理中文语境时效果更好

7.7 Verbose 模式(step_callback)

7.7.1 什么是 Verbose 模式?

Verbose 模式让 Agent 把"心里想什么"实时告诉用户:

7.7.2 回调构建

7.7.3 ReAct 循环中的回调触发

7.7.4 适用范围

场景
verbose 推送
原因
单聊(p2p)
✅ 推送
用户主动开启,希望看到推理过程
群聊(group)
✅ 推送
群成员可以看到推理过程
话题群(thread)
❌ 不推送
避免话题污染(一个话题里塞太多消息)
Sub-Crew
❌ 不注入
Sub-Crew 的推理过程不需要暴露给用户

7.8 工厂模式(build_agent_fn)

7.8.1 为什么需要工厂模式?

先看一个"不用工厂模式"的问题:
CrewAI 的 Agent 在 kickoff() 后会积累内部状态(如执行历史、工具调用记录)。如果复用同一个实例,上一轮的状态会影响下一轮的推理。这就是"状态污染"。

7.8.2 工厂模式的实现

7.8.3 闭包原理

通俗理解:闭包就像一个"带记忆的函数"。它记住了创建时的环境(sender、max_history_turns 等),之后每次调用都能使用这些记忆。

7.9 Crew 构建详解(_build_crew)

7.9.1 完整实现

7.9.2 关键参数说明

参数
为什么这样设置
temperature
0.3
较低温度让输出更稳定。对于需要精确执行的工作助手,不可预测性是有害的
process
Process.sequential
单 Agent 单 Task,顺序执行即可
verbose
True
在控制台打印 ReAct 过程,方便调试
output_pydantic
MainTaskOutput
强制 JSON 格式输出,程序可解析
max_iter
50
足够的迭代次数处理复杂任务,同时防止无限循环

7.9.3 IntermediateTool 详解


7.10 完整执行流程

把以上所有部分串联起来,看一次完整的消息处理流程:

7.11 本章小结

本章我们深入学习了 XiaoPaw 的 Agent 层——Main Crew 的完整实现。核心要点回顾:
概念
说明
位置
Agent
有身份、有目标、有工具的 AI 角色
agents.yaml + main_crew.py
Task
具体的任务描述和期望输出
tasks.yaml + main_crew.py
Crew
Agent + Task 的组合体
main_crew.py_build_crew()
ReAct 循环
Thought → Action → Observation 的推理循环
CrewAI 框架内部
step_callback
每轮 ReAct 后触发的回调,用于 verbose 推送
_make_step_callback()
工厂模式
每次请求创建新 Crew 实例,防止状态污染
build_agent_fn()
关键设计决策
决策
原因
单工具原则(SkillLoaderTool)
主 Agent 极简,能力通过 Skill 扩展
YAML + Python 分离
人设可调(YAML),代码稳定(Python)
结构化输出(output_pydantic)
程序可解析,避免 LLM 输出格式不一致
工厂模式(每次新建 Crew)
防止 CrewAI 内部状态污染
历史截断 + history_reader 引导
控制上下文长度,不丢失历史
低温 LLM(temperature=0.3)
追求稳定性和可预测性
下一步:下一章将深入 Sub-Crew 与 AIO-Sandbox 沙盒的实现,了解 Skill 如何在隔离环境中安全执行。

7.12 练习

  1. 修改 agents.yaml 中的 backstory,添加一条新的行为约束(如"回复不能超过 200 字"),观察 Agent 行为变化
  1. MainTaskOutput 中添加一个新字段 confidence: float,让 Agent 输出对自己回答的信心程度
  1. 尝试把 temperature 从 0.3 改为 0.8,观察 Agent 的回复风格变化
 

第8章:Sub-Crew 与 AIO-Sandbox

本章目标:深入理解 Sub-Crew 的创建、MCP 协议、AIO-Sandbox 沙盒通信,以及工具行为约束。
对应源码:

8.1 Sub-Crew 设计理念

8.1.1 为什么要 Sub-Crew?

主 Agent 的上下文窗口有限(LLM 处理输入有 token 上限),如果让主 Agent 直接操控所有的文件处理、搜索、飞书工具:

8.1.2 Sub-Crew 的隔离性

维度
主 Crew
Sub-Crew
LLM 上下文
完整历史的最近20条
完全独立,不知道主对话
工具
SkillLoaderTool + IntermediateTool
Sandbox MCP 工具
生命周期
每次用户消息新建
每个 task 型 Skill 调用新建
对话历史
不分享
不获取
文件系统
不直接访问
/workspace/sessions/{sid}/

8.2 AIO-Sandbox 沙盒概述

8.2.1 什么是 AIO-Sandbox?

AIO-Sandbox 是一个开源的 Docker 沙盒环境,提供:
  • 隔离的代码执行环境(Python、Bash)
  • 文件系统操作
  • 浏览器自动化
  • MCP(Model Context Protocol)接口

8.2.2 MCP 协议

MCP 是 Anthropic 提出的标准化工具调用协议。在 XiaoPaw 中:

8.2.3 Docker 挂载映射


8.3 Sub-Crew 构建(skill_crew.py)

8.3.1 build_skill_crew() 工厂

8.3.2 沙盒行为指令(sandbox_directive)

指令设计要点
指令
目的
工作目录/输出目录
约束文件路径,防止写入随机位置
可用工具列表
明确 Sub-Crew 能调用什么
规则 1:不编造结果
防止 LLM 幻觉
规则 3:输出到指定目录
统一输出路径,方便主 Agent 定位结果
规则 6:--break-system-packages
解决 Docker 内 pip 安装冲突

8.3.3 MCP 工具集

Sub-Crew 连接沙盒时,启用所有 MCP 工具(不使用白名单)。工具约束通过 Agent backstory 行为规则来管理。
为什么不使用 create_static_tool_filter 白名单?
  1. web_browse Skill 需要 browser_* 系列工具,静态白名单会限制这些工具
  1. 白名单后期维护成本高,新增工具需要修改代码
  1. Backstory 行为约束更灵活,可以根据 Skill 类型调整指令

8.4 Sub-Crew 调用流程(完整串联)

8.4.1 从 SkillLoaderTool 到 Sub-Crew

8.4.2 时序图


8.5 Sub-Crew 与主 Agent 的 Session

关键点
  • 同一个 session 的两个 Sub-Crew 共享 /workspace/sessions/s-abc/ 目录
  • 第一个 Sub-Crew 的输出文件可以被第二个 Sub-Crew 读取
  • 每个 Sub-Crew 是独立实例,不知道前一个 Sub-Crew 做了什么
  • 任务上下文(task_context)由主 Agent 负责串联

8.6 Sandbox 凭证注入

8.6.1 write_feishu_credentials()

8.6.2 使用方式

feishu_ops Skill 的 Python 脚本读取凭证:
为什么凭证不经过 LLM?
  1. LLM 的输入/输出可能被记录到日志和 trace
  1. 凭证是敏感信息,不应暴露给第三方模型
  1. 凭证通过文件系统注入沙盒,脚本直接读取
  1. LLM 不知道凭证的存在,也无法读取

8.7 本章小结

  • Sub-Crew:每次 task 型 Skill 调用创建新实例,完全隔离
  • AIO-Sandbox:Docker 沙盒,通过 MCP 协议通信
  • MCP 工具集:21 个工具全部可用,约束通过 backstory 行为规则
  • 凭证安全:飞书/百度凭证写入沙盒文件(chmod 600),不经过 LLM
  • 工作目录:Sandbox 路径与主机路径通过 Docker volume 双向映射
下一章将深入 SkillLoaderTool 的实现和 Skills 生态系统。
 

 

第9章:Skills 生态系统与 SkillLoaderTool

本章目标:全面理解 XiaoPaw 的 Skills 生态——从定义规范到渐进式披露,从 SkillLoaderTool 的主/子工具切换,到三种特殊处理(reference、task、history_reader)。你将会学到:Skill 是什么、如何定义、如何加载、如何执行,以及整个调用链路是如何衔接的。
对应源码:

9.1 什么是 Skill?—— 用一个比喻理解

在正式进入代码之前,我们先用一个生活中的比喻来理解 "Skill" 这个概念。

9.1.0 比喻:智能手机的应用商店

想象你的手机刚出厂时,只有打电话、发短信等最基本的功能。但当你从应用商店下载了微信、支付宝、地图等 App 后,手机的能力就大大扩展了。
XiaoPaw 的 Skill 生态 就是这个思路:

9.1.1 为什么需要 Skill 生态?

核心问题:如果让主 Agent 直接拥有所有工具(PDF 处理、Word 处理、搜索、飞书操作……),会发生什么?
关键优势
维度
全挂工具
Skill 生态
工具描述 token 消耗
~550 tokens(所有工具)
~150 tokens(只有 Skill 列表)
选择准确度
容易选错
更精准(先在菜单中选,再加载详细指令)
扩展性
新增工具需改 Agent 定义
只需添加 SKILL.md 文件
隔离性
一个工具崩溃影响全局
每个 Skill 在独立 Sub-Crew 中运行
上下文安全
工具描述可能泄露敏感信息
阶段 0 只暴露名称+简短描述

9.2 Skill 定义规范

9.2.1 SKILL.md 格式详解

每个 Skill 目录下必须有一个 SKILL.md 文件。这个文件是 Skill 的"身份证" + "使用说明书",分为两部分:
YAML Frontmatter 是 Skill 的"身份证"——描述 Skill 的基本信息,阶段 0 只需解析这部分。 Markdown 正文 是 Skill 的"使用说明书"——详细的执行步骤,阶段 1 按需加载。

实例:pdf Skill 的 SKILL.md

Python Libraries

pypdf - Basic Operations

...(省略更多内容)
不太好的 description 示例

9.2.3 Skill 的类型:reference vs task

这是 Skill 生态中最核心的概念之一。两种类型的设计目标不同:
维度
reference(参考型)
task(任务型)
含义
返回操作指南给主 Agent,主 Agent 自己推理
启动独立 Sub-Crew,在沙盒中执行
执行环境
主 Agent 的推理空间
独立的 AIO-Sandbox 沙盒
上下文
共享主 Agent 上下文
完全隔离,不感知主对话
开销
轻量,只是文本返回
较重,需要启动 Sub-Crew + MCP 通信
适用场景
信息型任务、需要主 Agent 综合判断
代码执行、文件处理、API 调用
示例
history_reader(读取历史消息)
pdfdocxfeishu_opsbaidu_search
通俗理解
  • reference 型 Skill = "给你一本说明书,你自己看着办"
  • task 型 Skill = "派一个专业工人去执行,你等着收结果就行"

9.2.4 load_skills.yaml — Skill 清单

skills/load_skills.yaml 是全局的 Skill 注册表,它告诉 SkillLoaderTool:有哪些 Skill 可用、各自的类型是什么、是否启用
关键设计点
  1. enabled 字段:可以临时关闭某个 Skill,不需要删除文件。比如某个 Skill 的 API Key 还没配置好,设 enabled: false 即可。
  1. type 字段:决定 Skill 的执行方式,SkillLoaderTool 据此分流。
  1. 注释:每个 Skill 的注释说明了为什么是 task 类型(需要代码执行),帮助维护者理解。
如何新增一个 Skill? 只需三步:
  1. 创建 skills/新Skill名/SKILL.md 文件
  1. load_skills.yaml 中添加条目
  1. 重启 XiaoPaw,SkillLoaderTool 自动发现新 Skill

9.3 渐进式披露(Progressive Disclosure)

9.3.1 设计原理——为什么分两阶段加载?

这是 SkillLoaderTool 最核心的设计模式。渐进式披露 的意思是:只在需要时才暴露详细信息
Token 预算分析

9.3.2 实现细节——_build_description() 方法

这是渐进式披露第一阶段的核心实现。它只读取 SKILL.md 的 YAML frontmatter,构建一个轻量级的 XML 格式工具描述:
为什么用 XML 格式?
XML 格式对 LLM 来说结构清晰,标签明确(<name><type><description>),LLM 能准确解析。相比之下,纯文本列表或 Markdown 表格可能被 LLM 误解格式。
路径穿越防护
这段代码的作用是:如果有人恶意配置 name="../../etc"resolve() 会计算出实际路径,然后 startswith 检查确保它仍然在 _SKILLS_DIR 目录下。这是一个重要的安全防护。

9.3.3 _extract_frontmatter_description() — 提取 frontmatter 中的 description

通俗解释
这个方法就像一个"图书管理员"——它只看书的封面(frontmatter),不打开书的内容(Markdown 正文)。这样就节省了时间和资源。

9.4 SkillLoaderTool 实现详解

9.4.1 类结构总览

9.4.2 PrivateAttr —— 为什么有些属性要用 PrivateAttr?

这是 CrewAI BaseTool 的一个重要机制。让我们用一个表格来对比:
属性类型
行为
用途
普通属性
被 CrewAI 序列化,传给 LLM
工具的名称、描述、参数 Schema
PrivateAttr
只在 Python 代码中使用,传递给 LLM
内部状态、会话 ID、凭据、缓存
为什么 session_id 不能暴露给 LLM?
_skill_registry_instruction_cache 为什么也是 PrivateAttr?

9.4.3 init 方法

9.4.4 输入输出模型

field_validator 的作用
通俗地说,这个 validator 就像一个"翻译官"。LLM 可能会传 task_context={"page": 1}(一个 dict),但我们的 Schema 定义的是 str 类型。validator 在 Pydantic 校验之前自动把 dict 转成 '{"page": 1}' 字符串,这样就不会报错了。

9.4.5 _get_skill_instructions() — 渐进式披露第二阶段

当 LLM 决定调用某个 Skill 时,SkillLoaderTool 需要加载完整的 SKILL.md 内容。这个方法就是渐进式披露第二阶段的核心:
关键设计点总结
  1. 缓存机制_instruction_cache 避免同一 Skill 重复读取文件
  1. 路径占位符替换{skill_base} → 实际的沙盒路径,让 SKILL.md 可以在不同环境复用
  1. 花括号转义{{{,防止 CrewAI 模板变量冲突
  1. sandbox_execution_directive:附加沙盒执行指令,告诉 Sub-Crew Agent 如何使用工具

9.5 三种处理类型详解

当 LLM 调用 SkillLoaderTool 时,_execute_skill_async() 方法会根据 Skill 的类型进行分流:

9.5.1 reference 型 — 返回指令文本

通俗解释:reference 型 Skill 就像"给你一本菜谱"。主 Agent 拿到菜谱后,自己理解步骤,自己决定怎么做。不需要派一个专业厨师(Sub-Crew)去执行。
适用场景
  • 信息型 Skill,如 history_reader
  • 需要主 Agent 综合判断的复杂决策流程
  • 操作指南长度适中,不会压满上下文窗口

9.5.2 task 型 — 启动 Sub-Crew

通俗解释:task 型 Skill 就像"叫一个专业服务"。比如你要处理 PDF,你自己不会,就叫一个"PDF 处理专家"(Sub-Crew)来帮你做。这个专家有自己的工具(沙盒 MCP 工具),做完后把结果告诉你。
关键点
  • 每次 build_skill_crew() 返回新实例,防止状态污染
  • Sub-Crew 在独立的沙盒中执行,与主 Agent 完全隔离
  • Sub-Crew 的结果通过 akickoff() 返回给主 Agent

9.5.3 history_reader — 特殊内联处理

history_reader 是一个特殊的 Skill:它虽然是 reference 型,但不需要从 SKILL.md 读取指令,而是直接在 SkillLoaderTool 内部从内存中读取历史消息
为什么 history_reader 要特殊处理?
原因
说明
不需要沙盒
只是内存中的列表切片操作,不涉及文件系统或代码执行
避免网络开销
不启动 Sub-Crew,不需要 MCP 协议通信,响应更快
安全性
历史消息包含用户隐私数据,不应传入沙盒环境
可靠性
减少中间环节,降低出错概率

9.5.4 三种处理类型对比


9.6 SKILL.md 模板变量安全

9.6.1 问题描述

CrewAI 框架使用 {var} 语法进行模板变量替换。当 CrewAI 加载 Agent 的 backstory 或 Task 的 description 时,会自动扫描所有 {变量名} 格式的占位符,然后在 akickoff(inputs={...}) 中查找对应的值。
问题来了SKILL.md 中可能包含代码示例,比如:
如果 CrewAI 发现 {name} 但没有在 inputs 中提供对应的值,就会报错:

9.6.2 解决方案

分两步走:
第一步:转义花括号
第二步:注入自映射变量
完整流程图
通俗解释
这就像"套娃"——先把 { 变成 {{(告诉 CrewAI "这是文本,别替换"),然后为了防止 CrewAI 的扫描器仍然报错,主动告诉它"这些变量都存在,值就是它们自己",这样 CrewAI 就不会报错了。

9.7 异步双通道 — _arun 与 _run

CrewAI 的 BaseTool 支持两种调用方式:异步 _arun() 和同步 _run()。SkillLoaderTool 同时实现了两者,确保在不同场景下都能正常工作。

9.7.1 异步路径 _arun()

9.7.2 同步路径 _run()

为什么需要两个通道?

9.8 完整调用流程 — 端到端走一遍

让我们用一个具体的例子来走一遍完整的调用流程:
场景:用户在飞书中说"帮我分析这个 PDF 文件的内容"

9.9 本章小结

本章我们深入学习了 XiaoPaw 的 Skills 生态系统和 SkillLoaderTool 的实现。核心要点回顾:
知识点
核心内容
Skill 定义
SKILL.md = YAML frontmatter(元数据)+ Markdown 正文(操作指南)
Skill 类型
reference(返回指令,轻量)vs task(启动 Sub-Crew,重量)
渐进式披露
阶段 0:只加载 frontmatter 的 description(~50 字符/Skill);阶段 1:按需加载完整 SKILL.md
SkillLoaderTool
主 Agent 的唯一工具,负责 Skill 发现、加载、分流
PrivateAttr
会话 ID、沙盒 URL 等敏感信息不暴露给 LLM
三种处理
reference(返回指令)、task(启动 Sub-Crew)、history_reader(内联分页)
模板变量安全
{{{ 转义 + 自映射注入,防止 CrewAI 报错
异步双通道
_arun()(异步路径,直接 await) + _run()(同步路径,ThreadPoolExecutor)
路径穿越防护
resolve() + startswith() 确保 Skill 路径在允许的目录内
设计哲学
  • 极简主 Agent:主 Agent 只有一个工具(SkillLoaderTool),所有能力通过 Skill 扩展
  • 渐进式披露:按需加载,不浪费 token
  • 安全第一:PrivateAttr 保护敏感信息,路径穿越防护,沙盒隔离
  • 可扩展性:新增 Skill 只需添加 SKILL.md 文件 + 更新 load_skills.yaml

9.10 练习与思考

  1. 新增一个 Skill:尝试创建一个名为 hello_world 的 task 型 Skill,让它能接收用户的名字并返回问候语。需要:
      • 创建 skills/hello_world/SKILL.md
      • load_skills.yaml 中注册
      • 理解 SkillLoaderTool 如何发现和加载它
  1. 对比 reference 和 taskhistory_reader 是 reference 型,但如果把它改成 task 型会有什么不同?分析两种方式的优缺点。
  1. 模板变量问题:如果 SKILL.md 中有 {user_name} 这样的占位符,当前的转义机制会把它变成什么?最终 Sub-Crew 看到的是什么?
  1. 路径安全:如果 load_skills.yaml 中配置了 name: "../../etc",SkillLoaderTool 会如何处理?画出代码执行路径。
  1. 渐进式披露的 token 节省:假设有 10 个 Skill,每个 SKILL.md 平均 3000 字符。计算渐进式披露相比全量加载节省了多少 token(假设 1 token ≈ 4 字符)。
下一章将详细介绍每个 Skill 的具体实现——包括文件处理、飞书操作、搜索、网页浏览等。
 

 

第10章:Skill 详解

本章目标:逐一深入分析每个 Skill 的设计目的、操作规范、脚本架构和实现细节。通过本章学习,你将理解每个 Skill 是如何独立工作、如何与沙盒互动、以及如何通过统一的输出规范与主 Agent 通信的。
对应目录:xiaopaw/skills/

10.1 分类总览与设计哲学

10.1.1 九大 Skill 一览

Skill
类型
使用沙盒
脚本语言
核心能力
pdf
task
Python
PDF 文本提取、合并、拆分、旋转、元数据读取
docx
task
Python
Word 文档创建、读取、编辑
pptx
task
Python
PowerPoint 文本提取
xlsx
task
Python
Excel 数据分析、读写、图表
feishu_ops
task
Python
飞书消息发送、文档读取、日历管理、多维表格
scheduler_mgr
task
Python
定时任务的创建/查看/更新/删除
baidu_search
task
Python
百度千帆网页搜索
web_browse
task
MCP 工具
网页抓取与浏览器自动化
history_reader
reference
无(内联)
对话历史按页读取

10.1.2 设计哲学

每个 Skill 都遵循以下设计原则:

10.2 文件处理 Skills

文件处理是 XiaoPaw 最基础的能力之一。当用户在飞书中上传 PDF、Word、Excel 或 PowerPoint 文件时,对应的 Skill 会被触发。

10.2.1 pdf — PDF 文本提取与处理

Skill 定义
通俗理解:PDF Skill 就像你的"PDF 全能助手"。你给它一个 PDF 文件,它能帮你提取文字、拆分页面、合并多个文件、旋转页面、提取表格等。
SKILL.md 中的核心操作指南
使用的 Python 库
用途
为什么选择它
pypdf
基础操作:读取、合并、拆分、旋转、元数据
纯 Python 实现,无外部依赖,安装简单
pdfplumber
高级操作:精确文本提取、表格提取
比 pypdf 的文本提取更准确,特别是复杂布局
为什么需要两个库?
pypdf 适合"编辑"操作(合并、拆分、旋转),pdfplumber 适合"提取"操作(文字、表格)。两者互补,覆盖了常见的 PDF 处理需求。
注意事项
  • 加密 PDF 需要密码才能处理
  • 扫描版 PDF(图片)需要 OCR 才能提取文字
  • 大文件(>100MB)处理可能较慢

10.2.2 docx — Word 文档处理

Skill 定义
通俗理解:DOCX Skill 就像你的"Word 助手"。它能读取 Word 文档的内容,也能根据你的需求创建新的 Word 文档。
核心操作
python-docx 库的核心概念
概念
对应 Word 中的...
代码操作
Document
整个 Word 文件
doc = Document(path)
Paragraph
一个段落
doc.add_paragraph("text")
Run
段落中的一段文字(相同格式)
run = para.add_run("text")
Table
表格
doc.add_table(rows, cols)
Heading
标题(1-9 级)
doc.add_heading("title", level=1)

10.2.3 pptx — PowerPoint 处理

Skill 定义
通俗理解:PPTX Skill 能帮你读取 PowerPoint 文件中的文字内容,提取每张幻灯片的标题和正文。
python-pptx 的核心概念
概念
代码
整个 PPT 文件
prs = Presentation(path)
一张幻灯片
slide = prs.slides[i]
幻灯片上的元素
shape = slide.shapes[j]
文本框
shape.text_frame
段落
shape.text_frame.paragraphs

10.2.4 xlsx — Excel 数据处理

Skill 定义
通俗理解:XLSX Skill 是你的"Excel 数据分析师"。它能读取 Excel 文件中的数据,进行筛选、排序、统计,并生成新的 Excel 文件。
为什么用 pandas?
pandas 是 Python 最强大的数据分析库。相比直接用 openpyxl 操作单元格,pandas 提供的"DataFrame"概念(类似 Excel 的表格视图)让数据分析更加直观和高效。

10.3 feishu_ops — 飞书操作

这是 XiaoPaw 最核心也是功能最丰富的 Skill,它让 Sub-Crew 能够直接与飞书平台交互——发送消息、读取文档、管理日历等。

10.3.1 脚本架构总览

10.3.2 共享认证模块 _feishu_auth.py

这是 feishu_ops 所有脚本的"地基"。每个脚本都通过 import _feishu_auth as auth 引用它,获取飞书 API 的认证 Token 和通用工具函数。
为什么需要一个共享模块?

10.3.3 消息发送脚本

send_text.py — 发送纯文字消息
这是最基础的消息发送脚本,也是理解其他脚本的起点:
参数说明
参数
必填
说明
示例
--routing_key
目标路由键,标识消息发给谁
p2p:ou_abc123(私聊)、group:oc_xyz789(群组)
--text
消息文本内容
"你好,这是定时报告"
send_image.py — 发送图片消息
发送图片比发送文字多一步:先上传图片获取 image_key,再用 image_key 发送图片消息。
send_file.py — 发送文件消息
这是处理结果回传的核心场景。用户上传文件 → Skill 处理 → 结果保存到 outputs/ → 调用 send_file.py 将结果发回给用户。

10.3.4 文档读取脚本

read_doc.py — 读取飞书文档
read_sheet.py — 读取飞书电子表格

10.3.5 所有脚本的统一设计模式

总结 feishu_ops 所有 16 个脚本的共同模式:

10.4 baidu_search — 百度搜索

10.4.1 Skill 概述

通俗理解:baidu_search Skill 是 XiaoPaw 的"搜索引擎"。当用户问"最近有什么新闻"、"Python 最新版本是什么"这类需要实时信息的问题时,这个 Skill 会去百度搜索最新结果。

10.4.2 凭证注入

搜索 API 需要 API Key。这个 Key 通过环境变量 BAIDU_API_KEY 配置,由 CleanupService 在启动时写入沙盒:

10.4.3 搜索脚本详解

参数说明
参数
必填
说明
示例
--query
搜索关键词或自然语言问题
"Python 异步编程最佳实践"
--top_k
返回结果数,默认 20,最大 50
--top_k 5(精准搜索)/ --top_k 30(广泛调研)
--recency
时间范围过滤
week(近7天)/ month(近30天)/ semiyear(近半年)/ year(近一年)
--sites
指定站点(逗号分隔)
"github.com,stackoverflow.com"
典型使用场景

10.5 web_browse — 网页浏览与自动化

10.5.1 Skill 概述

通俗理解:web_browse Skill 是 XiaoPaw 的"浏览器"。它能打开网页、提取内容、截图,甚至填写表单、点击按钮——就像真人在操作浏览器一样。

10.5.2 两种工作模式

web_browse 支持两种模式,根据任务需求选择:
模式
工具
速度
适用场景
快速提取
sandbox_convert_to_markdown
快(~2s)
提取网页文本内容(新闻文章、文档页面)
浏览器全功能
browser_navigate + browser_* 系列
慢(~10s)
截图、表单填写、JavaScript 动态渲染页面
为什么需要两种模式?

10.5.3 浏览器自动化工具流程

10.5.4 浏览器工具速查表

工具
功能
必填参数
可选参数
browser_navigate
打开 URL
url
-
browser_get_markdown
获取页面 Markdown
-
-
browser_get_text
获取页面纯文本
-
-
browser_read_links
获取所有链接
-
-
browser_screenshot
截图
name
fullPage, selector
browser_get_clickable_elements
获取可交互元素
-
-
browser_click
点击元素
index
-
browser_form_input_fill
填写输入框
index, value
clear
browser_select
下拉选择
index, value
-
browser_press_key
按键
key
-
browser_scroll
滚动页面
amount
-
browser_evaluate
执行 JavaScript
script
-
browser_close
关闭浏览器
-
-

10.6 scheduler_mgr — 定时任务管理

10.6.1 Skill 概述

通俗理解:scheduler_mgr Skill 是 XiaoPaw 的"闹钟管理器"。你可以通过它设置定时任务——比如"每天早上 9 点发送工作摘要"、"每周五下午 5 点生成周报"。

10.6.2 三种触发模式详解

Cron 表达式语法
常用 Cron 表达式示例
表达式
含义
0 9 * * *
每天 9:00
0 9 * * 1-5
工作日 9:00
0 9 1 * *
每月 1 号 9:00
*/30 * * * *
每 30 分钟
0 9,18 * * *
每天 9:00 和 18:00

10.6.3 任务数据结构

10.6.4 热加载机制

重要提示:scheduler_mgr Skill 只负责管理定时任务配置,不负责执行。触发时的业务处理由主 Agent 配合其他 Skill 完成。payload.message 必须是自然语言指令,描述"要做什么"。

10.7 history_reader — 历史阅读器

10.7.1 Skill 概述

通俗理解:history_reader 是 XiaoPaw 的"记忆回溯器"。当主 Agent 的上下文窗口只能容纳最近 20 条消息时,如果用户问"你之前说的 xxx 是什么意思",history_reader 可以翻出更早的对话记录。

10.7.2 为什么是 reference 型?

history_reader 是唯一一个 reference 型的 Skill,它在 SkillLoaderTool 中内联实现(详见第 9 章),不需要沙盒或 Sub-Crew。
原因
  1. 不需要沙盒:只是内存中的列表分页操作,不涉及文件系统或代码执行
  1. 避免网络开销:不启动 Sub-Crew,不需要 MCP 协议通信
  1. 安全性:历史消息包含用户隐私数据,不应传入沙盒环境
  1. 可靠性:减少中间环节,降低出错概率

10.7.3 使用场景

  • 用户询问"回顾一下我们之前的对话"
  • 历史截断后,Agent 需要查阅更早的消息
  • 用户询问特定历史话题("你上次说的那个 Python 库叫什么来着?")

10.8 本章小结

Skill
核心能力
关键技术
凭证来源
pdf
PDF 文本提取与处理
pypdf, pdfplumber
docx
Word 文档创建与编辑
python-docx
pptx
PowerPoint 文本提取
python-pptx
xlsx
Excel 数据分析
pandas, openpyxl
feishu_ops
飞书消息/文档/日历
飞书 REST API + 16 个独立脚本
/workspace/.config/feishu.json
baidu_search
百度搜索
百度千帆 Web Search API
/workspace/.config/baidu.json
web_browse
网页浏览与自动化
sandbox_convert_to_markdown + browser_* 工具
scheduler_mgr
定时任务管理
读写 tasks.json + CronService 热加载
history_reader
历史分页读取
内联实现,内存分页
设计亮点
  • 统一脚本模式:所有 task 型 Skill 脚本遵循相同的参数解析 → 认证 → API 调用 → 响应检查 → 结果输出模式
  • 凭证隔离:敏感信息通过文件系统注入沙盒,不经过 LLM
  • 独立可测试:每个脚本可以独立运行和测试,不依赖整个系统
  • 热加载:scheduler_mgr 的配置变更无需重启

10.9 练习与思考

  1. 新增一个消息类型:在 feishu_ops 中新增一个 send_markdown.py 脚本,支持发送 Markdown 格式的消息。参考 send_text.py 和 send_post.py 的实现。
  1. 搜索优化:baidu_search 的 _get_api_key() 每次都读取文件。如果改成缓存机制,应该如何设计?参考 feishu_ops 的 token 缓存思路。
  1. 文件处理链:用户上传了一个 Excel 文件,要求"分析数据并生成 PDF 报告"。这需要 chain 哪些 Skill?画出完整的调用链路。
  1. 错误处理:如果 baidu_search 的 API Key 过期了,脚本会输出什么?Sub-Crew Agent 会如何处理?主 Agent 会给用户什么回复?
  1. Cron 表达式:用 Cron 表达式表示"每月最后一天 18:00"和"每周一、三、五 9:00"。
下一章将深入 LLM 适配器的实现——如何将阿里云的通义千问模型接入 CrewAI 框架。

 

第11章:LLM 适配器

本章目标:深入理解 CrewAI 的 LLM 接口机制,掌握 AliyunLLM 适配器的实现细节——重试机制、函数调用(Function Calling)、多模态图片输入、消息格式化、工具结果截断等。

11.1 为什么需要自定义 LLM 适配器?

11.1.1 通俗理解

想象一下,你去国外旅行,发现当地的插座和你的充电器插头不匹配。你需要一个"转换插头"(适配器)才能给你的设备充电。
在 XiaoPaw 中,CrewAI 框架期望一个"标准插座"(OpenAI 格式),而阿里云通义千问(Qwen)提供的是一个"不同形状的插头"(DashScope API)。AliyunLLM 就是这个"转换插头"——它把 CrewAI 的请求翻译成 DashScope 能理解的格式,再把 DashScope 的响应翻译回 CrewAI 能理解的结果。

11.1.2 为什么不能直接用 OpenAI 的适配器?

特性
OpenAI
DashScope(通义千问)
API 端点
api.openai.com
dashscope.aliyuncs.com
认证方式
Bearer sk-xxx
Bearer sk-xxx(相同)
多地域
cn(国内)/ intl(国际)/ finance(金融云)
多模态模型
gpt-4o
qwen3-vl-plus(需单独指定)
Tool Calls 格式
标准 JSON
兼容,但部分参数需要规范化
上下文窗口
基于模型名
基于模型名(long→200K, max/plus/turbo→131K)

11.1.3 核心差异:多模态处理

最大的差异在于多模态图片处理。CrewAI 对工具返回结果的 stringify(字符串化)处理,会导致图片的 base64 编码被当作普通文本,DashScope 无法识别。AliyunLLM 需要反向还原这些字符串为多模态格式。

11.2 CrewAI BaseLLM 接口

11.2.1 接口定义

CrewAI 提供了 BaseLLM 抽象类,自定义 LLM 需要继承它并实现以下方法:

11.2.2 消息格式

CrewAI 传入 call() 的消息是已转换为 dict 格式的对话历史:

11.2.3 两种返回值模式

call() 方法可能返回两种不同的值,取决于 LLM 的决定:
当返回 tool_calls,CrewAI 会接管执行流程:
  1. CrawlAI 的 executor 接收 tool_calls
  1. 根据 function.name 找到对应的工具函数
  1. 执行工具函数,获得结果
  1. 将结果作为 tool 消息追加到对话历史
  1. 再次调用 call(),让 LLM 基于工具结果继续推理

11.3 AliyunLLM 完整实现

11.3.1 类结构总览

11.3.2 初始化详解

为什么重试次数默认是 2?
这是一个经验值。太少(1次)可能不够覆盖偶发性的网络问题;太多(5次以上)会让用户等待过久。2 次重试(共 3 次尝试)在"可靠性"和"响应速度"之间达到了较好的平衡。

11.3.3 多地域支持


11.4 核心流程:call() 方法

11.4.1 调用流程全景图

11.4.2 重试机制详解

重试是 AliyunLLM 最关键的可靠性保障机制。不同于简单的"失败就重试",它根据错误类型采取不同的策略:
重试策略总结
HTTP 状态码
含义
是否重试
原因
200
成功
-
正常响应
4xx
客户端错误(参数错误等)
❌ 不重试
请求本身有问题,重试不会改变结果
429
限流(Rate Limit)
✅ 重试
配额可能在下个时间窗口恢复
5xx
服务器错误
✅ 重试
服务端临时故障,重试大概率成功
Timeout
超时
✅ 重试
可能是网络抖动

11.4.3 空内容重试

LLM 偶尔会返回空内容(content=""),这通常是因为模型限流或偶发异常。AliyunLLM 对这个情况也做了专门处理:
为什么 _retry_on_empty 要设为 False?
这防止了无限递归。如果设为 True,空内容重试内部再次遇到空内容,又会触发重试,形成死循环。设为 False 后,内层重试再遇到空内容就会直接抛出异常,终止循环。

11.5 消息格式化

11.5.1 消息校验

在发送请求前,AliyunLLM 会校验消息格式:

11.5.2 工具结果截断

当工具返回数据过大时(如 Excel 解析结果可能包含大量文本),直接发送会导致 API 返回 500 错误。AliyunLLM 会自动截断过长的工具结果:

11.6 Function Calling 详解

11.6.1 什么是 Function Calling?

Function Calling(函数调用/工具调用)是 LLM 的一项核心能力。它允许 LLM 在推理过程中"调用"外部函数来获取信息或执行操作。
通俗理解:就像你问助手"今天天气怎么样",助手不会凭空编造,而是拿起手机查看天气预报 App,然后把结果告诉你。LLM 的 Function Calling 就是这个过程——它知道自己不能回答某些问题,所以"调用"外部工具来获取信息。

11.6.2 完整调用流程

11.6.3 两种 tool_calls 处理模式

AliyunLLM 的 call() 方法根据 available_functions 参数是否为 None,采取不同的处理方式:
为什么有两种模式?
模式
谁执行工具
适用场景
模式 1(递归)
AliyunLLM 自己
LLM 内部的工具调用循环
模式 2(返回)
CrewAI executor
CrewAI Agent 的工具调用流程
模式 2 是主要使用的方式。CrewAI 故意传 available_functions=None,让 LLM 只返回原始 tool_calls,由 CrewAI 的 _handle_native_tool_calls 统一执行。这样 CrewAI 可以控制工具的生命周期、记录 trace、管理回调等。

11.6.4 MCP 参数规范化

MCP(Model Context Protocol)工具对参数类型有严格要求。LLM 有时会把列表参数传成字符串 "None",导致 schema 校验失败。AliyunLLM 在返回 tool_calls 前做规范化处理:

11.6.5 递归 Function Calling

available_functions 不为 None 时,AliyunLLM 会递归处理工具调用:

11.7 多模态图片处理

11.7.1 问题背景

CrewAI 对工具返回结果做 stringify(字符串化)时,会将图片的 base64 编码当作普通文本处理。但 DashScope 的视觉模型需要图片以特定的多模态格式传入。

11.7.2 多模态检测与还原

11.7.3 模型自动切换

当检测到图片时,AliyunLLM 自动将 model 切换为 image_model
为什么需要切换模型?
通义千问的文本模型(如 qwen3.6-max-preview)不支持图片输入。必须使用专门的视觉模型(如 qwen3-vl-plus)才能处理图片。这个切换是自动的,对调用方透明。
![多模态模型切换流程]

11.8 上下文窗口大小

什么是 Token?
Token 是 LLM 处理文本的最小单位。粗略地说:
  • 1 个中文字 ≈ 1.5-2 个 tokens
  • 1 个英文单词 ≈ 1-1.5 个 tokens
  • 一段代码 ≈ 更多 tokens(各种符号都是独立的 token)

11.9 异步调用

为什么用 asyncio.to_thread() 而不是 aiohttp
方案
优点
缺点
asyncio.to_thread()
代码简单,复用同步逻辑
占用线程池资源
aiohttp
真正异步,不占用线程
需要重写所有 HTTP 逻辑
对于 XiaoPaw 的并发量(通常只有几个同时进行的对话),线程池的开销可以忽略不计,而代码简洁性更重要。

11.10 调试模式

当遇到 LLM 调用问题时,可以开启调试模式查看完整的请求内容:
调试模式下,日志会输出完整的请求 payload:

11.11 配置使用

_build_crew() 中创建 Agent 时指定 LLM:

11.12 本章小结

功能
实现方式
关键参数
多地域支持
3 个端点映射
region: cn / intl / finance
重试机制
按状态码分类重试
retry_count: 默认 2
空内容重试
递归调用,最多 2 次
_retry_on_empty + _empty_retry_count
Function Calling
两种模式(递归/返回)
available_functions
MCP 参数规范化
字符串 "None" → []
_MCP_LIST_PARAMS
工具结果截断
超 12000 字符自动截断
LLM_TOOL_RESULT_MAX_CHARS
多模态图片
检测 + 还原 + 模型切换
image_model: qwen3-vl-plus
消息校验
检查 role + tool_call_id
_validate_messages
上下文窗口
基于模型名推断
get_context_window_size
调试模式
环境变量控制
QWEN_DEBUG_PAYLOAD=1

11.13 练习与思考

  1. Token 计算:一条 1000 字的中文消息大约消耗多少 tokens?如果上下文窗口是 131,072 tokens,大约能容纳多少条这样的消息?
  1. 重试策略:为什么 4xx 错误不重试,而 5xx 错误要重试?如果 API 返回 408(Request Timeout),应该怎么处理?
  1. 多模态处理:如果用户同时上传了 3 张图片,_normalize_multimodal_tool_result 能否正确处理?如果不能,应该如何改进?
  1. 工具结果截断:如果 LLM 收到截断的工具结果后,仍然尝试基于不完整的信息做结论,会导致什么问题?截断后缀中的提示信息是否足够?
  1. 模型选择:什么时候应该用 qwen-long(200K 上下文)而不是 qwen-max(131K 上下文)?多消耗的 tokens 成本是否值得?
  1. 自定义适配器:如果要把 XiaoPaw 改为使用 OpenAI 的 GPT-4,需要修改 AliyunLLM 的哪些部分?哪些部分可以复用?
下一章将深入 CronService 定时任务调度系统——如何实现精确的定时触发、任务热加载和调度状态管理。
 

 

第12章:CronService 定时任务调度

本章目标:深入理解 CronService 的调度模型、三种调度模式(at / every / cron)、tick 主循环、mtime 热加载机制、任务状态持久化,以及由定时任务触发的消息如何在 Runner 中流转。
对应源码:

12.1 调度模型

12.1.1 通俗理解

CronService 就像你的"闹钟管家"。你告诉它"每天早上 9 点提醒我开会",它就会准时在你设定的时间"响铃"——把一条消息注入到 XiaoPaw 的处理管道中,就像有人给你发了消息一样。

12.1.2 数据模型

12.1.3 三种调度模式详解


12.2 CronService 核心实现

12.2.1 类结构

12.2.2 生命周期管理

12.2.3 主循环详解

这是 CronService 最核心的部分。每 0.05 秒执行一次 tick:

12.2.4 触发逻辑

为什么 msg_id 和 sender_id 是伪值?
因为这不是飞书用户的消息,而是定时服务产生的。使用特殊的 msg_id 和 sender_id 可以:
  • 日志中容易区分来源
  • 避免与真实飞书消息 ID 冲突
  • 支持去重(如果同一任务被重复触发)

12.2.5 触发后状态更新

12.2.6 Cron 表达式计算


12.3 热加载(Hot-Reload)

12.3.1 为什么需要热加载?

定时任务不是静态配置——用户可以通过 scheduler_mgr Skill 动态创建、修改、删除任务。这些变更写入 tasks.json 后,CronService 需要立即感知,而不是等下次重启。

12.3.2 mtime 检测机制

12.3.3 加载逻辑

12.3.4 原子持久化


12.4 tasks.json 格式


12.5 完整调用链路


12.6 为什么不用 asyncio 精确定时器?

初版 CronService 使用了 asyncio.wait_for(self._sleep.wait(), timeout=...) 的精确等待方式,但后来改为简单的 tick 轮询。原因如下:
方案
精度
代码复杂度
可靠性
精确定时器
微秒级
需要处理 wakeup 和各种边界情况
Tick 轮询
50ms
简单可靠,不易出错
对于定时任务来说,50ms 的精度完全够用(用户不会因为 50ms 的延迟而感知到差异),但代码简洁性和可靠性大大提升。

12.7 本章小结

组件
功能
关键实现
CronSchedule
调度配置
三种模式互斥:at / every / cron
CronPayload
触发内容
routing_key + message(自然语言)
CronState
运行状态
next_run / last_run / 状态 / 错误
CronService._loop()
主循环
每 50ms tick,检查到期任务
CronService._fire()
触发任务
构造伪 InboundMessage → Runner
CronService._check_mtime()
热加载检测
mtime + 文件大小双重比较
CronService._save_store()
原子持久化
write-then-rename
_next_cron_ms()
Cron 计算
croniter 库 + 时区支持
设计亮点
  • 统一处理管道:定时消息和用户消息走完全相同的 Runner 链路
  • 热加载:mtime 检测 + 立即重载,无需重启进程
  • 原子写入:write-then-rename 防止文件损坏
  • disabled 保留:禁用的任务不丢失,方便后续重新启用
  • cron 重新计算:每次加载都重新计算 cron 的 next_run,确保表达式变更立即生效

12.8 练习与思考

  1. 精度分析:如果 tick_interval 从 50ms 改为 1000ms(1 秒),对定时任务的精度有什么影响?什么场景下可以接受?
  1. 原子写入:为什么 _save_store 使用 write-then-rename 而不是直接写入?如果写入过程中进程崩溃,两种方式分别会有什么后果?
  1. cron 表达式:写出以下场景的 cron 表达式:
      • 每月 15 号 10:00
      • 每周一、三、五 9:00 和 18:00
      • 工作日每小时的第 30 分钟
  1. 错误处理:如果 _fire() 中 dispatch 失败,last_status 会被设为 "error"。这个错误信息对用户有什么用?如何改进错误报告?
  1. 并发安全:如果两个任务同时到期,_fire() 是顺序执行还是并发执行?如果是顺序执行,会不会导致后面的任务延迟触发?
  1. 时区处理:如果用户在日本(UTC+9)创建了一个 cron 任务 0 9 * * *,但 tasks.json 中 tz 写的是 "Asia/Shanghai"(UTC+8),实际触发时间是什么?如何避免这种问题?
下一章将深入可观测性与日志系统——如何监控 XiaoPaw 的运行状态、记录 LLM 调用轨迹、暴露 Prometheus 指标。
 
💡
欢迎您在底部评论区留言,一起交流~
上一篇
第一节 大脑:重新认识你自己
下一篇
临时
Loading...
0%