# 赋予 Discourse AI 代理技能

**URL:** https://meta.discourse.org/t/giving-discourse-ai-agent-skills/408666
**Category:** Extras
**Tags:** ai
**Created:** [2026年七月28日 05:40 UTC](https://meta.discourse.org/t/giving-discourse-ai-agent-skills/408666 "2026-07-28T05:40:40Z")
**Posts on this page:** 1
**Showing post:** 1

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [2026年七月28日 05:40 UTC](https://meta.discourse.org/t/giving-discourse-ai-agent-skills/408666/1 "2026-07-28T05:40:40Z")

</div>

在行业中，一种非常常见的模式被广泛应用于各种代理（Agent）中，那就是 **Agent Skills** （代理技能）。

> **[Agent Skills Overview - Agent Skills](https://agentskills.io/home)**
>
> A standardized way to give AI agents new capabilities and expertise.

技能解决了代理的 **上下文** 问题。

代理技能允许渐进式披露，而不是将所有指令和想法塞进一个巨大的系统提示（system prompt）中。

代理知道它能做什么，并调用非常具体的工具来查找如何执行。

这在 Discourse 中提供了显著优势， **因为** 我们可以将技能 **目录** 保留在主题（topics）中，并获得良好的编辑器、历史记录和权限管理。

通过在 Discourse AI 中使用 [自定义工具](https://meta.discourse.org/t/ai-bot-custom-tools/314103)，我们可以利用它们提供的两个非常强大的功能来近似实现这一系统。

- 自定义工具允许修改系统提示
- 自定义工具可以读取特定类别中的主题和帖子

### 我们的目标

为了在 Discourse 上近似实现技能，我们希望：

1. 每个技能对应一个主题
2. 在系统提示中自动生成可用的技能目录

因此，例如，我们希望主题具有如下结构（原始格式）：

```plaintext
---
name: doc-coauthoring
description: 引导用户通过结构化的工作流进行文档协作。当用户想要撰写提案、技术规格、决策记录或类似文档时使用。
---

# 文档协作工作流

1. 从用户那里收集上下文。
2. 确定大纲。
3. 起草并完善每个部分。
4. 从新读者的角度测试文档。

```

将其转换为系统提示中的以下内容：

```xml
<available_skills>
  <skill>
    <name>doc-coauthoring</name>
    <description>引导用户通过结构化的工作流进行文档协作...</description>
    <location>https://example.com/t/skill-doc-coauthoring/123</location>
  </skill>
  ... 此处有更多技能 ...
</available_skills>

```

我们选择这种特定格式， **因为** 许多语言模型已经针对这种特定结构进行了微调，这可以提高召回率。请注意，实际技能的主体内容是缺失的。

然后，如果模型检测到用户试图进行文档协作工作流，它将调用自定义工具 `load_skill` 以获取实际指令。

在此基础上，我们可以在 Discourse 上定义一个专用类别，包含我们希望代理使用的技能，并允许社区成员对其进行迭代改进。

### 它是如何工作的？

`load_skill` 的全部逻辑均使用自定义工具在 JavaScript 中编写。

`discourse.filterTopics` 可用于查找特定类别中的主题  
`discourse.getPost` 可用于获取帖子正文  
`customSystemMessage` 用于注入自定义系统提示

有了这些构建块

首先，我们定义一个新的自定义工具：

- 名称：load\_skill
- 参数：
  - “name”: “available\_skills 中的技能名称”

接下来，我们教授技能解析：

```javascript
var SKILLS_CATEGORY_SLUG = "agent-skills";
var SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;

function skillTopics() {
  var result = discourse.filterTopics({
    q: "category:" + SKILLS_CATEGORY_SLUG + " order:created",
    limit: 200,
  });

  return result && Array.isArray(result.topics) ? result.topics : [];
}

function topicOp(topic) {
  if (!topic || !topic.first_post_id) {
    return null;
  }

  return discourse.getPost(topic.first_post_id);
}

function frontmatter(raw) {
  if (typeof raw !== "string") {
    return null;
  }

  var match = raw.match(
    /^---[\t]*\n([\s\S]*?)\n---(?:[\t]*\n|$)/
  );

  return match ? match[1] : null;
}

function scalarField(yaml, field) {
  var match = yaml.match(
    new RegExp("^" + field + ":[\\t]*(.+)[\\t]*$", "m")
  );

  return match ? match[1].trim() : null;
}

function xmlEscape(value) {
  return String(value)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/\"/g, "&quot;")
    .replace(/'/g, "&apos;");
}

function skillRecord(topic) {
  var post = topicOp(topic);
  var yaml = post && frontmatter(post.raw);

  if (!yaml) {
    return null;
  }

  var name = scalarField(yaml, "name");
  var description = scalarField(yaml, "description");

  if (
    !name ||
    !description ||
    !SKILL_NAME_PATTERN.test(name) ||
    name.length > 64 ||
    description.length > 1024
  ) {
    return null;
  }

  return {
    name: name,
    description: description,
    location: discourse.baseUrl + topic.url,
    content: post.raw,
  };
}

function availableSkills() {
  var records = [];
  var topics = skillTopics();

  for (var i = 0; i < topics.length; i++) {
    var record = skillRecord(topics[i]);
    if (record) {
      records.push(record);
    }
  }

  records.sort(function (a, b) {
    return a.name.localeCompare(b.name);
  });

  return records;
}

```

请确保将 `SKILLS_CATEGORY_SLUG` 设置为包含您技能的实际类别别名（slug）。

接下来，我们使用 customSystemMessage 注入系统提示

```javascript
function customSystemMessage() {
  var skills = availableSkills();

  if (skills.length === 0) {
    return null;
  }

  var lines = [
    "Skills provide specialized instructions and workflows for specific tasks.",
    "Use the load_skill tool to load a skill when a task matches its description.",
    "<available_skills>",
  ];

  skills.forEach(function (skill) {
    lines.push(" <skill>");
    lines.push(" <name>" + xmlEscape(skill.name) + "</name>");
    lines.push(
      " <description>" +
        xmlEscape(skill.description) +
        "</description>"
    );
    lines.push(
      " <location>" + xmlEscape(skill.location) + "</location>"
    );
    lines.push(" </skill>");
  });

  lines.push("</available_skills>");
  return lines.join("\n");
}

```

最后，我们构建技能加载器，从别名到技能内容。

```javascript
function invoke(parameters) {
  var requestedName = parameters && parameters.name;

  if (typeof requestedName !== "string") {
    return "No skill name was provided.";
  }

  requestedName = requestedName.trim();

  if (!SKILL_NAME_PATTERN.test(requestedName)) {
    return "Skill names must use lowercase letters, numbers, and hyphens.";
  }

  var matches = availableSkills().filter(function (skill) {
    return skill.name === requestedName;
  });

  if (matches.length === 0) {
    return "No available skill has the name " + requestedName + ".";
  }

  if (matches.length > 1) {
    return "More than one skill has the name " + requestedName + ".";
  }

  var skill = matches[0];

  return [
    '<skill_content name="' + xmlEscape(skill.name) + '">',
    "# Skill: " + skill.name,
    "",
    skill.content.trim(),
    "",
    "Source topic for this skill: " + skill.location,
    "</skill_content>",
  ].join("\n");
}

```

完整的可工作代理 + 自定义工具 JSON 可在以下位置获取，您可以将其导入您的实例并根据需要进行修改：

[category-skills-agent.json](https://meta.discourse.org/uploads/short-url/bYc3FXcFxk5iglwIRw1edDjTOTk.json) (5.9 KB)

### 查看实际效果

 ![image](https://global.discourse-cdn.com/meta/original/4X/7/9/e/79ee2f02f6e0203ecba08eb7e3582e0a42374f91.jpeg)

 ![image](https://global.discourse-cdn.com/meta/original/4X/f/7/f/f7fc8f26e22531193f560589093546a6c7d6e669.png)

 ![image](https://global.discourse-cdn.com/meta/original/4X/2/4/2/24285f748a83d570174f0d4ae510c247a02ccfb3.png)

### 局限性

当前的演示仍然存在 N+1 调用问题，即在每次用户交互时加载每个带有技能的帖子。在大规模场景下，这可能不可取。

可以修改设计，以便通过工作流自动生成技能目录帖子，或者扩展 `filterTopics` 以允许进行前缀查询调用。

此外，代理技能支持资源和脚本，资源可以通过允许“读取”工具并从技能链接到特定主题来近似实现。Shell 执行显然超出了 Discourse 的范围。

希望这对您有所帮助

---

_[View the full topic](https://meta.discourse.org/t/giving-discourse-ai-agent-skills/408666)._
