Un modèle très courant dans l’industrie, utilisé par une grande variété d’agents, est celui des Compétences d’Agent (Agent Skills)
Les compétences résolvent un problème de contexte pour les agents.
Au lieu de bourrer toutes vos instructions et idées dans un unique et gigantesque prompt système, les compétences d’agent permettent une divulgation progressive.
Votre agent sait ce qu’il peut faire et appelle un outil très spécifique pour savoir comment le faire.
Cela offre des avantages significatifs sur Discourse parce que nous pouvons conserver le catalogue de compétences dans des sujets et bénéficier d’un bon éditeur, d’un historique et de permissions.
En utilisant des outils personnalisés dans Discourse AI, nous pouvons approximer ce système grâce à deux fonctionnalités très puissantes qu’ils offrent.
- Les outils personnalisés permettent la modification du prompt système
- Les outils personnalisés peuvent lire des sujets et des messages dans des catégories particulières.
Notre Objectif
Pour approximer les compétences sur Discourse, nous souhaitons
- Un sujet par compétence
- Un répertoire automatique de compétences disponibles dans le prompt système
Ainsi, par exemple, nous voudrions un sujet de cette forme (brut) :
---
name: doc-coauthoring
description: Guide les utilisateurs à travers un workflow structuré pour la co-rédaction de documentation. À utiliser lorsque l'utilisateur souhaite rédiger une proposition, une spécification technique, un registre de décision ou un document similaire.
---
# Workflow de co-rédaction de document
1. Recueillir le contexte auprès de l'utilisateur.
2. Se mettre d'accord sur un plan.
3. Rédiger et affiner chaque section.
4. Tester le document du point de vue d'un nouveau lecteur.
À traduire dans ce qui suit dans le prompt système :
<available_skills>
<skill>
<name>doc-coauthoring</name>
<description>Guide les utilisateurs à travers un workflow structuré pour la co-rédaction de documentation...</description>
<location>https://example.com/t/skill-doc-coauthoring/123</location>
</skill>
... plus de compétences ici ...
</available_skills>
Nous choisissons ce format particulier parce que de nombreux modèles de langage sont déjà affinés pour rechercher cette forme spécifique, ce qui peut améliorer le rappel. Notez que le corps de la compétence réelle est absent.
Ensuite, si le modèle détecte qu’un utilisateur essaie de co-rédiger un workflow de document, il appellera l’outil personnalisé load_skill pour obtenir les instructions réelles.
Avec cela en place, nous pouvons définir une catégorie dédiée sur Discourse qui contient les compétences que nous voulons que nos agents utilisent et permettre aux membres de la communauté d’itérer dessus.
Comment ça marche ?
Toute la logique de load_skill est rédigée en JavaScript, en utilisant un outil personnalisé.
discourse.filterTopics peut être appelé pour rechercher des sujets dans une catégorie particulière
discourse.getPost peut être appelé pour obtenir le corps d’un message
customSystemMessage est utilisé pour injecter un prompt système personnalisé
Avec ces blocs de construction
D’abord, nous définissons un nouvel outil personnalisé :
- nom : load_skill
- params :
- “name” : “Le nom de la compétence depuis available_skills”
Ensuite, nous enseignons l’analyse des compétences :
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;
}
Assurez-vous de définir SKILLS_CATEGORY_SLUG sur le slug réel de la catégorie contenant vos compétences.
Ensuite, nous utilisons customSystemMessage pour injecter le prompt système
function customSystemMessage() {
var skills = availableSkills();
if (skills.length === 0) {
return null;
}
var lines = [
"Les compétences fournissent des instructions et des workflows spécialisés pour des tâches spécifiques.",
"Utilisez l'outil load_skill pour charger une compétence lorsqu'une tâche correspond à sa 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");
}
Enfin, nous construisons le chargeur de compétence qui va du slug au contenu de la compétence.
function invoke(parameters) {
var requestedName = parameters && parameters.name;
if (typeof requestedName !== "string") {
return "Aucun nom de compétence n'a été fourni.";
}
requestedName = requestedName.trim();
if (!SKILL_NAME_PATTERN.test(requestedName)) {
return "Les noms de compétences doivent utiliser des lettres minuscules, des chiffres et des tirets.";
}
var matches = availableSkills().filter(function (skill) {
return skill.name === requestedName;
});
if (matches.length === 0) {
return "Aucune compétence disponible n'a le nom " + requestedName + ".";
}
if (matches.length > 1) {
return "Plus d'une compétence a le nom " + requestedName + ".";
}
var skill = matches[0];
return [
'<skill_content name="' + xmlEscape(skill.name) + '">',
"# Compétence : " + skill.name,
"",
skill.content.trim(),
"",
"Sujet source pour cette compétence : " + skill.location,
"</skill_content>",
].join("\n");
}
Un agent complet fonctionnel + json d’outil personnalisé est disponible ici, vous pouvez l’importer dans votre instance et le modifier comme vous le souhaitez :
category-skills-agent.json (5,9 Ko)
Voir cela en action
Limitations
La démo actuelle effectue encore un appel N+1 en chargeant chaque message avec une compétence à chaque tour d’utilisateur. À grande échelle, cela peut ne pas être conseillé.
La conception pourrait être modifiée pour qu’un message de catalogue de compétences soit généré automatiquement à l’aide d’un workflow ou peut-être que filterTopics pourrait être étendu pour permettre un appel demandant un préfixe.
De plus, les compétences d’agent prennent en charge les ressources et les scripts, les ressources pourraient être approximées en permettant l’outil “read” et en liant à des sujets spécifiques depuis votre compétence. L’exécution de shell est clairement hors de portée pour Discourse.
J’espère que vous trouverez cela utile


