Appel d'outils : laisser le modèle agir
Le modèle ne peut ni consulter lui-même des données en temps réel, ni exécuter la moindre action. Lui donner un vrai outil, qui cherche la dernière version d'un paquet sur PyPI, et parcourir tout le déroulé d'un appel d'outils, y compris les appels parallèles, la gestion des erreurs et les précautions en mode réflexion.
- Environ 45 minutes
- Niveau : Intermédiaire
- Testé : 2026-09-14 deepseek-flash
Le code et les sorties des programmes sont reproduits tels qu’ils ont tourné : commentaires et sorties sont donc en chinois.
Demandez au modèle « quelle est la dernière version de httpx ? » : il ne peut répondre que d'après ses données d'entraînement, avec un numéro de version vieux d'un an, ou inventé. Demandez-lui « ajoute une réunion à mon agenda » : il ne peut que répondre « C'est fait », et rien ne se passe.
Le modèle ne sait produire que du texte. Pour qu'il obtienne des données en temps réel et agisse vraiment, il faut lui donner des outils : vous écrivez des fonctions et lui dites lesquelles il peut utiliser ; quand il le juge nécessaire, il produit « je veux appeler telle fonction avec tels paramètres » ; votre programme exécute réellement la fonction et lui communique le résultat ; le modèle répond à l'utilisateur d'après ce résultat. Ce mécanisme s'appelle l'appel d'outils (tool calling), aussi appelé appel de fonctions (function calling).
C'est la base des agents du module 05 ; cette leçon en éclaircit chaque étape.
Le déroulé
D'abord la vue d'ensemble. Une question-réponse avec appel d'outils demande au moins deux appels au modèle :
你的程序 模型
│ 1. 用户问题 + 工具说明书 │
│ ────────────────────────────────────────────▶ │
│ 2. "请调用 get_pypi_info(httpx)" │
│ ◀──────────────────────────────────────────── │
│ 3. 程序自己执行 get_pypi_info("httpx") │
│ 拿到结果 {"version": "0.28.1", ...} │
│ 4. 之前的全部消息 + 工具结果 │
│ ────────────────────────────────────────────▶ │
│ 5. "httpx 的最新版本是 0.28.1" │
│ ◀──────────────────────────────────────────── │
L'essentiel se joue aux étapes 2 et 3 : le modèle n'exécute jamais aucun code. Il ne fait que produire une « demande d'appel » structurée ; le pouvoir d'exécution reste entièrement à votre programme. Vous pouvez vérifier ce qu'il veut appeler et si les paramètres sont corrects, puis décider d'exécuter ou de refuser. C'est capital pour la sécurité ; la leçon 8 du module 05 y revient en détail.
Étape 1 : écrire l'outil et sa description
Un outil est une simple fonction Python. En voici une réellement utilisable : elle appelle l'interface publique de PyPI pour obtenir la dernière version d'un paquet.
import httpx
def get_pypi_info(package: str) -> dict:
"""真正干活的函数:调用 PyPI 的公开接口。"""
r = httpx.get(f"https://pypi.org/pypi/{package}/json", timeout=10)
if r.status_code == 404:
return {"error": f"PyPI 上没有叫 {package} 的包"}
info = r.json()["info"]
return {"name": info["name"], "version": info["version"], "summary": info["summary"],
"requires_python": info["requires_python"]}
Au passage, c'est justement httpx qui envoie ici la requête HTTP ; il a été installé comme dépendance avec openai.
On écrit ensuite une « description » qui informe le modèle de l'existence de cet outil. Le modèle ne voit pas le code de votre fonction, il ne voit que cette description :
TOOLS = [
{
"type": "function",
"function": {
"name": "get_pypi_info",
"description": "查询一个 Python 包在 PyPI 上的最新版本、简介和支持的 Python 版本。",
"parameters": {
"type": "object",
"properties": {
"package": {"type": "string", "description": "PyPI 上的包名,例如 httpx"},
},
"required": ["package"],
},
},
}
]
FUNCTIONS = {"get_pypi_info": get_pypi_info}
name est le nom de l'outil, description dit ce qu'il fait, parameters décrit les paramètres en JSON Schema. Le modèle décide quand utiliser l'outil d'après description, et comment remplir les paramètres d'après parameters. La qualité de la description détermine directement s'il saura s'en servir et s'il s'en servira correctement ; la leçon 3 du module 05 explique comment l'écrire.
FUNCTIONS associe le nom de chaque outil à la vraie fonction ; à la réception d'une demande d'appel, le programme s'en sert pour trouver la fonction à exécuter.
Étape 2 : la boucle
messages = [{"role": "user", "content": "httpx 和 requests 在 PyPI 上的最新版本分别是多少?各自要求什么 Python 版本?"}]
for step in range(1, 6): # 最多 5 轮,防止意外的死循环
response = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=TOOLS,
extra_body={"thinking": {"type": "enabled" if THINKING else "disabled"}},
)
message = response.choices[0].message
print(f"第 {step} 轮:finish_reason={response.choices[0].finish_reason}")
if not message.tool_calls:
print("最终回答:", message.content)
break
# 把模型的这条消息原样放回历史。开思考时,里面的 reasoning_content 也必须带上
messages.append(message.model_dump(exclude_none=True))
for call in message.tool_calls:
args = json.loads(call.function.arguments)
print(f" 模型要求调用 {call.function.name}({args})")
try:
result = FUNCTIONS[call.function.name](**args)
except Exception as e: # 工具出错也要告诉模型,而不是让程序崩掉
result = {"error": f"{type(e).__name__}: {e}"}
print(f" 返回:{result}")
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result, ensure_ascii=False)})
À chaque tour : appeler le modèle avec tools. Si la réponse ne contient pas de tool_calls, le modèle a donné sa réponse finale, on s'arrête. Sinon, on les exécute un par un, on ajoute les résultats à l'historique sous forme de messages de role tool, et on rappelle le modèle.
Quelques points d'attention :
- Remettre la demande d'appel du modèle dans l'historique.
messages.append(message.model_dump(exclude_none=True))rajoute tel quel le message du modèle contenanttool_calls. Sans cette étape, au tour suivant, le modèle voit un tas de résultats d'outils sans savoir qui les a demandés. - Faire correspondre les
tool_call_id. Chaque demande d'appel a unid, et le résultat correspondant doit porter le mêmetool_call_id. Quand le modèle demande plusieurs outils d'un coup, c'est ainsi qu'il sait quel résultat répond à quelle demande. - Les paramètres sont du JSON sous forme de chaîne.
call.function.argumentsest une chaîne comme'{"package": "httpx"}', à analyser avecjson.loads. - Une erreur d'outil ne doit pas faire planter le programme. Renvoyez le message d'erreur au modèle comme résultat ; il saura souvent s'adapter, par exemple réessayer avec un autre paramètre, ou dire honnêtement à l'utilisateur qu'il n'a rien trouvé.
- Fixer un nombre maximal de tours. Le modèle peut appeler des outils en boucle sans s'arrêter ; la limite est l'ultime garde-fou.
Résultat
第 1 轮:finish_reason=tool_calls
模型要求调用 get_pypi_info({'package': 'httpx'})
返回:{'name': 'httpx', 'version': '0.28.1', 'summary': 'The next generation HTTP client.', 'requires_python': '>=3.8'}
模型要求调用 get_pypi_info({'package': 'requests'})
返回:{'name': 'requests', 'version': '2.34.2', 'summary': 'Python HTTP for Humans.', 'requires_python': '>=3.10'}
第 2 轮:finish_reason=stop
最终回答: 两个包在 PyPI 上的最新信息如下:
| 包名 | 最新版本 | 要求 Python 版本 | 简介 |
|---|---|---|---|
| **httpx** | 0.28.1 | >=3.8 | The next generation HTTP client. |
| **requests** | 2.34.2 | >=3.10 | Python HTTP for Humans. |
几点说明:
- **httpx** 支持范围更宽,Python 3.8 及以上都能用,兼容性更好。
- **requests** 这边要求 Python 3.10 及以上,门槛更高一些。
- 光看"最低版本要求"的话,httpx 覆盖的老版本 Python 更多;但如果你跑在 3.10+ 环境上,两者都没问题。
如果你告诉我项目所用的 Python 版本,我可以帮你判断具体该选哪个。
(Ce sont les versions trouvées le 14 septembre 2026 ; au moment où vous l'exécuterez, les versions sur PyPI auront peut-être changé.)
Au tour 1, finish_reason vaut tool_calls : c'est le troisième cas du tableau de la leçon 3 du module 00. Et dans ce même tour, le modèle a demandé deux appels : un pour httpx, un pour requests. C'est l'appel d'outils parallèle : le modèle juge que les deux recherches sont indépendantes et les demande ensemble, ce qui économise un aller-retour. Au tour 2, le modèle dispose de deux données réelles et donne sa réponse finale ; les numéros de version viennent tous de PyPI, il ne les a pas inventés.
Avec le mode réflexion activé
Les modèles de DeepSeek activent la réflexion par défaut. Pour utiliser des outils en mode réflexion, une règle : le reasoning_content de chaque tour précédent doit être renvoyé tel quel à l'API. Sans outils, le renvoyer ou non n'a pas d'importance, le serveur l'ignore ; avec des outils, c'est obligatoire.
Le code ci-dessus transforme tout le message du modèle en dictionnaire avec message.model_dump(exclude_none=True) et le remet dans l'historique, ce qui inclut naturellement reasoning_content : il fonctionne donc aussi avec la réflexion :
python tool_calling.py --think
第 1 轮:finish_reason=tool_calls
模型要求调用 get_pypi_info({'package': 'httpx'})
返回:{'name': 'httpx', 'version': '0.28.1', 'summary': 'The next generation HTTP client.', 'requires_python': '>=3.8'}
模型要求调用 get_pypi_info({'package': 'requests'})
返回:{'name': 'requests', 'version': '2.34.2', 'summary': 'Python HTTP for Humans.', 'requires_python': '>=3.10'}
第 2 轮:finish_reason=stop
最终回答: 两个包在 PyPI 上的最新信息如下:
(后面的回答内容和不开思考时相近,这里省略)
Une façon d'écrire courante consiste à ne récupérer que content et tool_calls pour construire à la main un dictionnaire remis dans l'historique. Cela marche sans réflexion, mais avec la réflexion, reasoning_content est perdu. Remettre le message tel quel avec model_dump est le plus simple.
Que faire si le modèle passe de mauvais paramètres
Les paramètres remplis par le modèle ne sont pas forcément corrects : un nom de paquet inexistant, un paramètre obligatoire manquant, un mauvais type. Plusieurs lignes de défense :
- Une description claire. Précisez format et exemple dans la
descriptiondu paramètre : « nom du paquet sur PyPI, par exemple httpx » vaut mieux que simplement « nom du paquet ». - Vérifier dans la fonction. Ne supposez pas que les paramètres sont valides : le nom du paquet contient-il des caractères étranges, la valeur est-elle dans une plage raisonnable ?
- Renvoyer l'erreur au modèle. Dans le code ci-dessus, toute exception levée par la fonction est interceptée et renvoyée sous forme de
{"error": "..."}. Pour un paquet introuvable sur PyPI, la fonction renvoie elle-même un message d'erreur. En voyant l'erreur, le modèle se corrige généralement ou le dit honnêtement à l'utilisateur. - Le mode strict. La leçon 4 du module 02 a présenté le mode strict de DeepSeek (
strict: true), qui garantit que les paramètres respectent la structure du schéma, mais pas que leur contenu est juste.
Problèmes courants
Le modèle aurait dû appeler l'outil, mais a inventé une réponse directement : vérifiez que la description de l'outil dit clairement ce qu'il fait. Vous pouvez aussi écrire dans le message system « pour toute information de version d'un paquet, utilise obligatoirement get_pypi_info, ne réponds pas de mémoire ». Si un outil doit impérativement être appelé, forcez-le avec tool_choice.
Erreur sur l'ordre des messages : généralement, un message tool n'est pas précédé du message assistant avec tool_calls correspondant, ou les tool_call_id ne correspondent pas. Suivez le code ci-dessus : d'abord le message du modèle, puis les résultats d'outils un par un.
Exercices
- Demandez un paquet qui n'existe pas sur PyPI, par exemple « quelle est la dernière version de httpxx ? », et regardez le message d'erreur renvoyé par l'outil et la façon dont le modèle répond à l'utilisateur.
- Ajoutez un outil
get_github_stars(repo)qui interroge l'interface publique de GitHubhttps://api.github.com/repos/{repo}pour obtenir le nombre d'étoiles d'un dépôt (pas besoin de clé, mais le nombre d'appels par heure est limité). Demandez « quelle est la dernière version de httpx et combien d'étoiles a-t-il sur GitHub ? », et voyez si le modèle appelle les deux outils à la fois. - Remplacez
messages.append(message.model_dump(exclude_none=True))par un dictionnaire écrit à la main ne contenant quecontentettool_calls, lancez avec--thinket voyez ce qui se passe.
Auto-test
1. Lors d'un appel d'outils, est-ce le modèle qui exécute la fonction get_pypi_info ?
Non. Le modèle produit seulement une demande structurée du type « je veux appeler get_pypi_info avec le paramètre httpx ». C'est votre programme qui exécute réellement la fonction. Le pouvoir d'exécution reste entièrement au programme, qui peut vérifier, modifier ou refuser la demande du modèle.
2. Le modèle demande deux outils d'un coup. Comment lui communiquer les deux résultats séparément ?
Chaque demande d'appel a un id unique. Ajoutez pour chaque appel un message de role tool, en mettant dans tool_call_id l'id de la demande correspondante ; le modèle s'en sert pour associer résultats et demandes.
3. À quoi faut-il faire attention en utilisant des outils avec le mode réflexion activé ?
Le reasoning_content de chaque message précédent du modèle doit être renvoyé tel quel à l'API. Le plus simple est de remettre tout le message du modèle dans l'historique avec message.model_dump(exclude_none=True), plutôt que de n'en extraire à la main que content et tool_calls.
Questions et discussion
Bloqué sur cette leçon ? Posez votre question ici. Et si vous pouvez répondre à quelqu'un, n'hésitez pas.
Une question rapporte 3 points, une réponse 6. Les messages paraissent après vérification.
Chargement de la discussion…