Discourse AI 에이전트에 스킬 부여하기

산업 전반에서 다양한 에이전트에 걸쳐 매우 일반적으로 사용되는 패턴 중 하나가 **에이전트 스킬(Agent Skills)**입니다.

스킬은 에이전트의 컨텍스트 문제를 해결합니다.

모든 지침과 아이디어를 거대한 단일 시스템 프롬프트에 쑤어 넣는 대신, 에이전트 스킬은 점진적 공개(progressive disclosure)를 가능하게 합니다.

에이전트는 자신이 할 수 있는 것을 알고, 이를 수행하는 방법을 찾기 위해 매우 구체적인 도구를 호출합니다.

이것은 Discourse에서 우리가 스킬 카탈로그를 토픽으로 유지하면서 편집기, 히스토리, 권한 관리 기능을 활용할 수 있기 때문에 상당한 이점을 제공합니다.

Discourse AI에서 커스텀 도구를 사용하면 두 가지 매우 강력한 기능을 제공하므로 이 시스템을 근사적으로 구현할 수 있습니다.

  • 커스텀 도구는 시스템 프롬프트 수정을 허용합니다.
  • 커스텀 도구는 특정 카테고리의 토픽과 게시물을 읽을 수 있습니다.

우리의 목표

Discourse에서 스킬을 근사적으로 구현하기 위해 우리는 다음을 원합니다.

  1. 스킬당 하나의 토픽
  2. 시스템 프롬프트 내 자동 생성되는 사용 가능한 스킬 디렉터리

예를 들어, 다음과 같은 형태(원본)의 토픽을 원합니다:

---
name: doc-coauthoring
description: Guide users through a structured workflow for co-authoring documentation. Use when the user wants to write a proposal, technical specification, decision record, or similar document.
---

# Document co-authoring workflow

1. Gather context from the user.
2. Agree on an outline.
3. Draft and refine each section.
4. Test the document from a new reader's perspective.
```\n
이것은 시스템 프롬프트에서 다음과 같이 변환되어야 합니다:

```xml
<available_skills>
  <skill>
    <name>doc-coauthoring</name>
    <description>Guide users through a structured workflow for co-authoring documentation...</description>
    <location>https://example.com/t/skill-doc-coauthoring/123</location>
  </skill>
  ... more skills here ...
</available_skills>

이러한 특정 형식을 선택한 이유는 많은 언어 모델이 이미 이 특정 형태를 찾는 데 파인 튜닝되어 있어 리콜(recall) 성능을 향상시킬 수 있기 때문입니다. 실제 스킬의 본문이 빠져 있음을 주목하십시오.

그런 다음, 모델이 사용자가 문서 공동 작성 워크플로를 시도하고 있음을 감지하면 실제 지침을 가져오기 위해 커스텀 도구 load_skill을 호출합니다.

이것이 설정되면, Discourse에서 에이전트가 사용하도록 원하는 스킬을 포함하는 전용 카테고리를 정의하고 커뮤니티 멤버들이 이를 반복적으로 개선할 수 있도록 허용할 수 있습니다.

작동 방식

load_skill의 전체 로직은 커스텀 도구를 사용하여 JavaScript로 작성됩니다.

discourse.filterTopics를 호출하여 특정 카테고리의 토픽을 검색할 수 있습니다.
discourse.getPost를 호출하여 게시물 본문을 가져올 수 있습니다.
customSystemMessage는 커스텀 시스템 프롬프트를 주입하는 데 사용됩니다.

이러한 빌딩 블록을 사용하여

먼저 새로운 커스텀 도구를 정의합니다:

  • 이름: load_skill
  • 매개변수:
    • “name”: “available_skills의 스킬 이름”

다음으로 스킬 파싱을 학습시킵니다:

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을 스킬이 포함된 카테고리의 실제 슬러그로 설정하는 것을 잊지 마십시오.

다음으로 customSystemMessage를 사용하여 시스템 프롬프트를 주입합니다.

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");
}

마지막으로, 슬러그에서 스킬 내용으로 이어지는 스킬 로더를 구성합니다.

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 (5.9 KB)

실제 작동 모습

한계

현재 데모는 여전히 모든 사용자 턴마다 스킬이 포함된 모든 게시물을 로드하는 N+1 호출을 수행합니다. 대규모 환경에서는 이것이 권장되지 않을 수 있습니다.

워크플로를 사용하여 스킬 카탈로그 게시물을 자동으로 생성하도록 설계를 수정하거나, filterTopics를 확장하여 접두사(prefix)를 요청하는 호출을 허용할 수 있습니다.

또한 에이전트 스킬은 리소스와 스크립트를 지원하며, 리소스는 “read” 도구를 허용하고 스킬에서 특정 토픽으로 링크를 걸어서 근사적으로 구현할 수 있습니다. 셸 실행은 명확하게 Discourse의 범위 밖입니다.

이 글이 유용하길 바랍니다

8개의 좋아요

오늘 하루 동안 이것을 조금씩 사용해 보고 있는데, 확실히 유용하다는 느낌을 받고 있습니다.

다양한 반복을 거치며 꽤 길어진 시스템 프롬프트를 가진 에이전트 하나를 가지고 있었는데, 이제 그것을 7개의 스킬로 나누어 각각을 별도의 토픽으로 구성했습니다.

“한계” 부분에서 암시하고 있는 몇 가지 개선 사항을 살펴보는 데 관심이 있습니다.

특히, 현재는 에이전트가 로드해야 할 스킬 목록을 에이전트의 시스템 프롬프트에 전부 나열하고 있습니다. 이를 자동으로 발견하게 하거나, 다른 스킬들을 링크하는 단일 스킬(토픽)을 가리키는 한 단계의 간접 참조를 두는 방식을 선호합니다.

처음에는 그 “간접 참조” 방식을 시도해 보았지만, 커스텀 시스템 메시지로 주입되도록 하는 데는 아직 완벽하게 작동하지 않는 것 같아 지금은 그 부분을 잠시 보류했습니다.

그러나 그렇게 되면 시스템 프롬프트를 수정하지 않고도 진행 중인 스킬 개발에 대해 더 협업적인 워크플로우를 가질 수 있을 것이므로, 그런 기능이 있다면 좋겠습니다.

정말 멋져 보입니다. 하지만 제 기술 수준으로는 아직 좀 부족합니다. 현재는 IDE 내에서 스킬과 에이전트를 사용 중입니다.

입문자에게 적합한 학습 경로가 있을까요? 제안을 환영합니다. LLM에게 묻기 전에 먼저 사람의 경험을 듣고 싶습니다.

1개의 좋아요

해당 게시물은 포괄적이며 모든 단계가 포함되어 있습니다. 하지만 그 마음을 이해합니다 …

  • SAM, 이건 정말 복잡해요!!!

@Falco 저와 AI 팀이 이 문제에 대해 고민하고 있습니다. “스킬” 사용 사례를 위해 Discourse AI에 새로운 최상위 개념을 직접 도입하는 것이 의미 있을 수 있습니다.

이것을 탐색해 봤을 때, 이는 단순한 스킬보다는 “그라운딩 콘텐츠(grounding content)” 유형의 개념에 더 가까웠습니다. 카테고리/태그나 심지어 원격 데이터 집합을 통해 에이전트를 그라운딩(grounding)하고 싶을 수 있기 때문입니다.

2개의 좋아요