Ein sehr häufiges Muster in der Branche, das in einer Vielzahl von Agenten verwendet wird, sind Agent Skills.
Skills lösen ein Kontext-Problem für Agenten.
Anstatt alle Ihre Anweisungen und Ideen in einen riesigen System-Prompt zu stopfen, ermöglichen Agent Skills eine progressive Offenlegung.
Ihr Agent weiß, was er tun kann, und ruft ein sehr spezifisches Tool auf, um zu erfahren, wie er es tun soll.
Dies bietet erhebliche Vorteile auf Discourse, weil wir den Skill-Katalog in Themen halten und einen schönen Editor, Historie und Berechtigungen erhalten können.
Mit benutzerdefinierten Tools in Discourse AI können wir dieses System aufgrund der zwei sehr leistungsstarken Funktionen, die sie bieten, approximieren.
- Benutzerdefinierte Tools ermöglichen die Änderung des System-Prompts.
- Benutzerdefinierte Tools können Themen und Beiträge in bestimmten Kategorien lesen.
Unser Ziel
Um Skills auf Discourse zu approximieren, möchten wir
- Ein Thema pro Skill
- Ein automatisch verfügbares Skill-Verzeichnis im System-Prompt
So möchten wir zum Beispiel ein Thema in dieser Form (raw):
---
name: doc-coauthoring
description: Führen Sie Benutzer durch einen strukturierten Workflow für die gemeinsame Erstellung von Dokumentation. Verwenden Sie dies, wenn der Benutzer einen Vorschlag, eine technische Spezifikation, eine Entscheidungsprotokoll oder ein ähnliches Dokument schreiben möchte.
---
# Dokument Co-Authoring Workflow
1. Sammeln Sie Kontext vom Benutzer.
2. Einigung auf einen Entwurf.
3. Entwerfen und Verfeinern Sie jeden Abschnitt.
4. Testen Sie das Dokument aus der Perspektive eines neuen Lesers.
In das Folgende im System-Prompt übersetzt werden:
<available_skills>
<skill>
<name>doc-coauthoring</name>
<description>Führen Sie Benutzer durch einen strukturierten Workflow für die gemeinsame Erstellung von Dokumentation...</description>
<location>https://example.com/t/skill-doc-coauthoring/123</location>
</skill>
... mehr Skills hier ...
</available_skills>
Wir wählen dieses bestimmte Format weil viele Sprachmodelle bereits darauf feinabgestimmt sind, nach dieser spezifischen Form zu suchen, was das Abrufen verbessern kann. Beachten Sie, wie der Körper des tatsächlichen Skills fehlt.
Wenn das Modell dann erkennt, dass ein Benutzer versucht, einen Dokument-Workflow zu co-autoren, wird es das benutzerdefinierte Tool load_skill aufrufen, um die tatsächlichen Anweisungen zu erhalten.
Mit diesem Setup können wir eine dedizierte Kategorie auf Discourse definieren, die die Skills enthält, die wir für unsere Agenten verwenden möchten, und Community-Mitgliedern ermöglichen, daran zu arbeiten.
Wie funktioniert es?
Die gesamte Logik von load_skill wird in JavaScript verfasst, unter Verwendung eines benutzerdefinierten Tools.
discourse.filterTopics kann aufgerufen werden, um nach Themen in einer bestimmten Kategorie zu suchen.
discourse.getPost kann aufgerufen werden, um einen Beitragskörper zu erhalten.
customSystemMessage wird verwendet, um einen benutzerdefinierten System-Prompt einzuspeisen.
Mit diesen Bausteinen
Zuerst definieren wir ein neues benutzerdefiniertes Tool:
- name: load_skill
- params:
- “name”: “Der Name des Skills aus available_skills”
Als nächstes lehren wir Skill-Parsing:
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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/\"/g, """)
.replace(/'/g, "'");
}
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;
}
Stellen Sie sicher, dass SKILLS_CATEGORY_SLUG auf den tatsächlichen Slug der Kategorie gesetzt ist, die Ihre Skills enthält.
Als nächstes verwenden wir customSystemMessage, um den System-Prompt einzuspeisen.
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");
}
Schließlich bauen wir den Skill-Loader, der vom Slug zum Inhalt des Skills geht.
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");
}
Ein vollständiger, funktionierender Agent + benutzerdefiniertes Tool JSON ist hier verfügbar, Sie können es in Ihre Instanz importieren und nach Bedarf anpassen:
category-skills-agent.json (5.9 KB)
Dies in Aktion sehen
Einschränkungen
Die aktuelle Demo hat immer noch einen N+1-Aufruf, der jeden Beitrag mit einem Skill bei jedem Benutzerzug lädt. Im großen Maßstab ist dies möglicherweise nicht ratsam.
Das Design könnte so geändert werden, dass ein Skill-Katalog-Beitrag automatisch mit einem Workflow generiert wird, oder vielleicht könnte filterTopics erweitert werden, um einen Aufruf mit Präfix zu ermöglichen.
Zusätzlich unterstützen Agent Skills Ressourcen und Skripte, Ressourcen könnten durch das Erlauben des “read”-Tools und das Verlinken zu bestimmten Themen aus Ihrem Skill approximiert werden. Shell-Ausführung ist eindeutig außerhalb des Rahmens für Discourse.
Ich hoffe, Sie finden dies hilfreich.


