技能优先架构
Themis 将每个 AI 智能体功能结构化为三个层次:技能(领域知识)、输出工具(结构化契约)和工作流(生命周期编排)。技能是基础 —— 它们承载了使智能体高效运作的判断力、推理能力和专业知识。
三层架构
+--------------------------------------------------+
| 第一层:技能 |
| 职责:角色、流程、判断、领域知识 |
| 来源:基于文件 (.claude/skills/, lib/skills/) |
| 数据库存储 (Skill 模型, 3 种范围) |
| 提示 (app/prompts/) |
+--------------------------------------------------+
|
智能体调用工具
|
v
+--------------------------------------------------+
| 第二层:输出工具 |
| 职责:结构化数据契约、副作用 |
| 位置:app/services/*_tool_builder.rb |
| 构建方式:ClaudeAgentSDK.create_tool() |
+--------------------------------------------------+
|
返回结果
|
v
+--------------------------------------------------+
| 第三层:工作流 |
| 职责:仅生命周期编排 |
| 位置:app/services/workflows/ |
| 目标:50 行以内 |
+--------------------------------------------------+
第一层:技能
技能拥有所有判断力。它们告诉智能体_做什么_、如何推理_以及_何时使用工具。Themis 支持三种技能来源,协同工作为智能体提供全面的领域知识。
基于文件的技能(代码库)
以 Markdown 文件形式提交到代码库中的技能,带有 SKILL.md 清单。两个目录服务于不同目的:
.claude/skills/—— Themis 内部技能。架构指南、编码规范、审查方法论、集成辅助。这些保留在 Themis 仓库中,由 Claude Agent SDK 自动发现。lib/skills/—— 可移植技能。在代码生成期间复制到目标项目工作树中,使智能体能够携带跨项目标准(例如code-quality/包含 Rails 规范和安全检查清单)。
每个技能是一个包含 SKILL.md(YAML 前置信息 + Markdown)和可选补充文件的目录:
.claude/skills/understanding-themis/
SKILL.md # 包含名称、描述、内容的清单
HOTWIRE.md # 补充参考资料(可选)
数据库存储技能(Skill 模型)
用户创建的技能存储在数据库中,带有 Active Storage 文件附件。通过 Web UI 或在对话中通过智能体工具管理。三种范围控制可见性:
| 范围 | 归属 | 可见范围 | 用例 |
|---|---|---|---|
| 系统 | 管理员 | 所有用户、所有空间 | 组织级标准 |
| 空间 | 空间 | 所有空间成员 | 团队特定知识 |
| 个人 | 用户 | 仅所有者 | 个人偏好和工作流 |
数据库技能在每次智能体运行前由 SkillExtractor 提取到磁盘,按范围缓存并进行原子目录替换。智能体通过相同的 .claude/skills/ 目录约定发现它们。
SkillExtractor.prepare_for_agent(space:, user:)
→ Queries Skill.available_for(user, space)
→ Extracts to cache dirs (system / space / personal)
→ Returns add_dirs array for SDK options
智能体还可以在对话中通过 SkillToolBuilder 工具(create_skill、update_skill、list_my_skills)创建和更新自己的个人技能。
智能体提示
提供工作流特定指令的静态和动态提示文件:
- 静态提示(
.md)—— 技能不需要运行时上下文时使用。示例:pr_review.md包含审查流程、质量标准和裁决准则。 - 动态提示(
.md.erb)—— 注入运行时数据的 ERB 模板。示例:base_agent.md.erb渲染每个 space 的上下文(如 agent 身份和可用渠道)。
提示位于 app/prompts/。通过 PromptLoader.load("name")(静态)或 PromptLoader.render("name", locals)(动态)加载。
提示定义流程和判断,但不描述输出格式 —— 该职责属于输出工具的 schema。
技能如何到达智能体
ChatJob / ChannelMentionJob
│
├─ System prompt ← PromptLoader (app/prompts/)
│
└─ add_dirs ← SkillExtractor
├─ File-based skills (.claude/skills/)
└─ DB skills (extracted to cache)
├─ system/
├─ {space_id}/space/
└─ {space_id}/personal/{user_id}/
Claude Agent SDK 扫描 add_dirs 中的 SKILL.md 文件并自动使其对智能体可用。技能可通过 feature_skills 设置按空间开关。
第二层:输出工具
输出工具定义智能体与系统之间的结构化契约。我们不要求智能体生成可解析的文本(这很脆弱),而是给它一个带有类型化参数的工具来调用。
将工具接入调用方是定义工具之后的下一步。每个智能体调用方(完整智能体、Web/API 对话、消息)通过工具目录选择性加入一组工具组 —— 将工具添加到新调用方是一次声明式更改,而不是跨四个文件的编辑。
工具构建器位于 app/services/*_tool_builder.rb,使用 ClaudeAgentSDK.create_tool():
class PRReviewToolBuilder
def self.build_submit_review_tool(review:, space:)
ClaudeAgentSDK.create_tool(
"submit_review",
"Submit your completed code review.",
{
type: "object",
properties: {
verdict: { type: "string", enum: %w[APPROVE REQUEST_CHANGES COMMENT] },
summary: { type: "string", description: "Markdown review summary" },
comments: {
type: "array",
items: {
type: "object",
properties: {
path: { type: "string" },
line: { type: "integer" },
body: { type: "string" }
},
required: %w[path line body]
}
}
},
required: %w[verdict summary]
}
) do |args|
# Side effects: submit to GitHub, update review record
end
end
end
Schema 就是格式规范。智能体在其工具列表中看到它,就确切知道要产生什么。不会在输出格式指令上浪费提示预算。
当前工具构建器
按用途分组。所有工具都通过工具目录接入智能体调用方。
工作流输出契约
| 构建器 | 关键工具 | 用途 |
|---|---|---|
PRReviewToolBuilder | get_pr_info, get_pr_diff, get_pr_comments, get_ci_status, submit_review | PR 审查工作流输出 |
CodeGenerationResultToolBuilder | submit_code_generation_result | 代码生成的 PR 元数据 |
AutomationToolBuilder | skip_message | 自动化跳过决策 |
触发器(工厂接线)
| 构建器 | 关键工具 | 用途 |
|---|---|---|
PRReviewTriggerToolBuilder | trigger_pr_review | 从对话/提及中入队 PR 审查 |
CodeGenerationToolBuilder | trigger_code_generation | 从对话/提及中入队代码生成 |
数据访问
| 构建器 | 关键工具 | 用途 |
|---|---|---|
GithubToolBuilder | get_pr_info, get_pr_diff, get_pr_comments, get_ci_status, list_pull_requests, post_pr_comment | 对话/提及上下文中的 GitHub 直接 API 访问 |
ChatHistoryToolBuilder | search_conversations, recall_conversation | 按需对话历史 |
RepoSearchToolBuilder | resolve_repo_path | 浏览本地 git 工作树 |
ThemisQueryToolBuilder | query_themis_data | Themis 数据库查询(editable_by? 门禁) |
GoogleDriveProxyToolBuilder | 代理的 Google Drive 读取工具 | 按用户的 OAuth 范围 Drive 访问 |
副作用
| 构建器 | 关键工具 | 用途 |
|---|---|---|
SentryToolBuilder | update_sentry_issue | Sentry 状态 + 分配 |
MemoryToolBuilder | save_memory, delete_memory | 按用户的内存存储 |
对话 UX
| 构建器 | 关键工具 | 用途 |
|---|---|---|
AskUserQuestionHook (PreToolUse) | AskUserQuestion (原生内置) | 结构化的澄清问题 — 不作为 MCP 工具构建,通过钩子拦截 |
FileToolBuilder | create_file | 智能体生成的文件下载 |
ShowWidgetToolBuilder | show_widget | 沙箱化 HTML 小组件(D3、Mermaid、SVG) |
ShowChartToolBuilder | show_chart | 结构化 Chart.js 渲染 |
ImageGenerationToolBuilder | generate_image | Gemini 图像生成 |
资源管理
| 构建器 | 关键工具 | 用途 |
|---|---|---|
SkillToolBuilder | 13 个工具 —— CRUD、文件操作、签出/签入 | 智能体驱动的技能管理 |
AutomationChatToolBuilder | create_automation, update_automation, list_my_automations, delete_automation | 智能体驱动的自动化管理 |
第三层:工作流
工作流是薄的胶水层。它创建记录、启动智能体、处理错误并更新状态。它应该包含零判断和零解析。
所有工作流继承自 Workflows::BaseWorkflow 并实现 #execute。基类提供 #run_agent(prompt:, system_prompt:, model:, max_turns:)。
module Workflows
class FeatureWorkflow < BaseWorkflow
def execute(input:)
record = FeatureRecord.create!(input: input, status: "running")
begin
result = run_agent(
prompt: build_prompt(input),
system_prompt: PromptLoader.load("feature_name")
)
record.complete!(result)
rescue => e
record.fail!(e.message)
raise
end
end
end
end
如果您的工作流正在进行正则解析、JSON 提取或业务逻辑 —— 说明某些内容放错了层。
决策框架
| 问题 | 答案 | 层 |
|---|---|---|
| 是否涉及判断、推理或领域知识? | 将其移到技能中 | 技能 |
| 是否定义结构化数据交换或产生副作用? | 将其做成输出工具 | 输出工具 |
| 是否管理记录生命周期、错误恢复或编排? | 保留在工作流中 | 工作流 |
| 正在从 LLM 自由文本中解析结构化数据? | 做法有误 | 重构为输出工具 |
| 正在编写关于输出格式的提示指令? | 工具 schema 应该处理这个 | 重构为输出工具 |
| 工作流超过 50 行? | 某些内容放错了层 | 审计并重新分配 |
添加新功能
步骤 1:编写技能
根据用途决定技能的存放位置:
- 智能体提示(
app/prompts/)—— 工作流特定的指令。静态使用.md,动态上下文使用.md.erb。 - 代码库技能(
.claude/skills/)—— 跨工作流共享的可复用领域知识。 - 数据库技能 —— 用户可配置的知识,通过 UI 管理。
关注:角色、流程、判断准则、领域知识。不要描述输出格式。
步骤 2:将输出契约定义为 SDK 工具
创建 app/services/feature_tool_builder.rb。工具 schema 定义智能体产出什么。处理器执行副作用。
class FeatureToolBuilder
def self.build_submit_tool(record:, space:)
ClaudeAgentSDK.create_tool(
"submit_result",
"Submit your analysis results.",
{ type: "object", properties: { ... }, required: %w[...] }
) do |args|
# Execute side effects, return confirmation
end
end
end
步骤 3:将工作流编写为薄胶水层
创建 app/services/workflows/feature_workflow.rb。它只应创建记录、构建提示、调用 run_agent、处理错误和更新状态。目标 50 行以内。
步骤 4:接入任务
创建 app/jobs/feature_job.rb。任务构建选项(模型、MCP 服务器、工具、技能目录),实例化工作流并调用 execute。使用工具目录选择性加入工具组,而不是手动组装 mcp_servers / allowed_tools 列表。
class FeatureJob < ApplicationJob
def perform(record_id)
record = FeatureRecord.find(record_id)
options = build_options(record)
workflow = Workflows::FeatureWorkflow.new(options: options)
workflow.execute(record: record)
end
end
反模式
| 反模式 | 为什么是错的 | 修复方法 |
|---|---|---|
| 从智能体自由文本解析 JSON | 脆弱:在 Markdown 围栏、额外文本、格式变化时会崩溃 | 定义带类型化参数的输出工具 |
| 提示指令描述输出格式 | 浪费 Token 预算在智能体可能忽略的规则上;重复契约 | 工具 schema 就是格式 |
| 工作流中的业务逻辑 | 将编排与领域逻辑耦合;使工作流臃肿 | 将判断移到技能中,数据契约移到工具中 |
| 没有 ERB 的大型提示 | 无法注入运行时上下文(用户偏好、项目设置) | 对动态部分使用 .md.erb 和 locals |
| 每个工作流多个输出工具 | 使智能体困惑于该调用哪个工具 | 每个工作流一个主要输出工具 |
| 工具处理器中的复杂业务逻辑 | 难以测试,与基础设施紧密耦合 | 保持处理器精简:验证、副作用、确认 |
工作流成熟度
成熟度跟踪每个工作流将技能、输出工具和编排分离的清晰程度。无论成熟度级别如何,所有工作流的工具接线现在都通过工具目录统一进行。
| 工作流 | 成熟度 | 输出契约 |
|---|---|---|
| PRReview | 高 | submit_review SDK 工具处理 GitHub 提交 |
| Mention | 高 | 智能体直接使用 MCP 工具(GitHub、Linear 评论) |
| Automation | 高 | skip_message 工具用于跳过决策;通过 AutomationMessageDeliveryService 投递 |
| CodeGeneration | 中 | submit_code_generation_result 工具已存在;PR 元数据部分通过解析获取 |
成熟度级别:
- 高 —— 提示拥有判断力,工具拥有契约,工作流是薄的生命周期胶水。
- 中 —— 部分遵循模式。仍有一些解析或格式指令。
- 低 —— 判断、解析和编排混杂在工作流中。