Перейти к содержимому
ПереводыQwen, InternLM и другие

Kimi-K2: руководство по вызову инструментов

Полный перевод tool call guidance из репозитория Kimi-K2 (Moonshot AI): цикл вызова инструментов через OpenAI SDK, сборка вызовов из потока и ручной разбор сырых спецтокенов модели — с примечаниями переводчика о двух ловушках в примерах.

Чтобы включить вызов инструментов, при запуске сервиса может понадобиться задать параметры парсера вызовов. Подробности — в deploy_guidance.

В Kimi-K2 процесс вызова инструментов выглядит так:

  • описания функций передаются в Kimi-K2;
  • Kimi-K2 решает вызвать функцию и возвращает пользователю информацию, нужную для вызова;
  • пользователь выполняет вызов, собирает результаты и передаёт их в Kimi-K2;
  • Kimi-K2 продолжает генерацию на основе результатов, пока не сочтёт, что информации достаточно, чтобы ответить пользователю.
sequenceDiagram
participant U as Ваш код
participant K as Kimi-K2
U->>K: messages + tools
loop пока finish_reason == "tool_calls"
  K->>U: tool_calls (имя, аргументы, id)
  U->>U: выполнить функции
  U->>K: сообщения role="tool" с результатами
end
K->>U: итоговый ответ (finish_reason = "stop")
Цикл вызова инструментов (схема переводчика)

Допустим, у нас есть функция get_weather, которая узнаёт погоду в реальном времени. Она принимает название города и возвращает погоду. Нужно подготовить для неё структурированное описание, чтобы Kimi-K2 понимала, что делает функция.

def get_weather(city):
return {"weather": "Sunny"}
# Собираем описания инструментов в tools
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather information. Call this tool when the user needs to get weather information",
"parameters": {
"type": "object",
"required": ["city"],
"properties": {
"city": {
"type": "string",
"description": "City name",
}
}
}
}
}]
# Отображение «имя инструмента → объект», чтобы потом было удобно вызывать
tool_map = {
"get_weather": get_weather
}

(Описание инструмента для модели переводится так: «Получить информацию о погоде. Вызывай этот инструмент, когда пользователю нужна информация о погоде»; параметр city — «название города».)

Мы используем openai.OpenAI, чтобы отправить Kimi-K2 сообщения вместе с описаниями инструментов. Kimi-K2 сама решает, пользоваться ли инструментами и как. Если Kimi-K2 считает, что нужен вызов инструмента, она вернёт результат с finish_reason='tool_calls'. В этом случае в результате есть информация о вызове. Выполнив вызовы по этой информации, нужно добавить их результаты в историю диалога и снова обратиться к Kimi-K2. Kimi-K2 может понадобиться вызывать инструменты несколько раз, пока модель не решит, что текущих результатов хватает для ответа на вопрос пользователя. Поэтому проверяем finish_reason до тех пор, пока он не перестанет быть tool_calls.

Результаты, которые пользователь получил от инструментов, добавляются в messages с role='tool'.

import json
from openai import OpenAI
model_name='moonshotai/Kimi-K2-Instruct'
client = OpenAI(base_url=endpoint,
api_key='xxx')
messages = [
{"role": "user", "content": "What's the weather like in Beijing today? Let's check using the tool."}
]
finish_reason = None
while finish_reason is None or finish_reason == "tool_calls":
completion = client.chat.completions.create(
model=model_name,
messages=messages,
temperature=0.3,
tools=tools,
tool_choice="auto",
)
choice = completion.choices[0]
finish_reason = choice.finish_reason
# Внимание: finish_reason по окончании вызовов инструментов в разных движках может отличаться, проверку нужно подстроить
if finish_reason == "tool_calls":
messages.append(choice.message)
for tool_call in choice.message.tool_calls:
tool_call_name = tool_call.function.name
tool_call_arguments = json.loads(tool_call.function.arguments)
tool_function = tool_map[tool_call_name]
tool_result = tool_function(tool_call_arguments)
print("tool_result", tool_result)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_call_name,
"content": json.dumps(tool_result),
})
print('-' * 100)
print(choice.message.content)

(Запрос пользователя: «Какая сегодня погода в Пекине? Давай проверим с помощью инструмента».)

Вызов инструментов работает и в режиме стримингапотоковая выдача (стриминг)EN streamingОтвет приходит по кусочкам через Server-Sent Events; уменьшает время до первого токена, но не число токенов.. Тогда нужно собирать информацию о вызове, приходящую в потоке, пока не наберётся полный вызов. Смотрите код ниже:

messages = [
{"role": "user", "content": "What's the weather like in Beijing today? Let's check using the tool."}
]
finish_reason = None
msg = ''
while finish_reason is None or finish_reason == "tool_calls":
completion = client.chat.completions.create(
model=model_name,
messages=messages,
temperature=0.3,
tools=tools,
tool_choice="auto",
stream=True
)
tool_calls = []
for chunk in completion:
delta = chunk.choices[0].delta
if delta.content:
msg += delta.content
if delta.tool_calls:
for tool_call_chunk in delta.tool_calls:
if tool_call_chunk.index is not None:
# Расширяем список tool_calls
while len(tool_calls) <= tool_call_chunk.index:
tool_calls.append({
"id": "",
"type": "function",
"function": {
"name": "",
"arguments": ""
}
})
tc = tool_calls[tool_call_chunk.index]
if tool_call_chunk.id:
tc["id"] += tool_call_chunk.id
if tool_call_chunk.function.name:
tc["function"]["name"] += tool_call_chunk.function.name
if tool_call_chunk.function.arguments:
tc["function"]["arguments"] += tool_call_chunk.function.arguments
finish_reason = chunk.choices[0].finish_reason
# Внимание: finish_reason по окончании вызовов инструментов в разных движках может отличаться, проверку нужно подстроить
if finish_reason == "tool_calls":
for tool_call in tool_calls:
tool_call_name = tool_call['function']['name']
tool_call_arguments = json.loads(tool_call['function']['arguments'])
tool_function = tool_map[tool_call_name]
tool_result = tool_function(tool_call_arguments)
messages.append({
"role": "tool",
"tool_call_id": tool_call['id'],
"name": tool_call_name,
"content": json.dumps(tool_result),
})
# Текст, сгенерированный вместе с вызовом инструмента, — не окончательный; сбрасываем msg
msg = ''
print(msg)

Главное в этом коде: вызов приходит кусочками. В первом фрагменте обычно есть id и имя функции, дальше аргументы прилетают по нескольку символов строки JSON. Поле index говорит, к какому из параллельных вызовов относится кусочек, поэтому мы склеиваем строки по индексу.

Запросы на вызов инструментов, которые генерирует Kimi-K2, можно разбирать и вручную — особенно полезно, если сервис, которым вы пользуетесь, не умеет разбирать вызовы. Вызовы в выводе Kimi-K2 обёрнуты в <|tool_calls_section_begin|> и <|tool_calls_section_end|>, каждый отдельный вызов — в <|tool_call_begin|> и <|tool_call_end|>. Идентификатор инструмента и аргументы разделены токеном <|tool_call_argument_begin|>. Формат идентификатора — functions.{func_name}:{idx}, из него можно извлечь имя функции.

По этим правилам можно напрямую отправить запрос в интерфейс completions и разобрать вызовы вручную.

import requests
from transformers import AutoTokenizer
messages = [
{"role": "user", "content": "What's the weather like in Beijing today? Let's check using the tool."}
]
msg = ''
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
while True:
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
tools=tools,
add_generation_prompt=True,
)
payload = {
"model": model_name,
"prompt": text,
"max_tokens": 512
}
response = requests.post(
f"{endpoint}/completions",
headers={"Content-Type": "application/json"},
json=payload,
stream=False,
)
raw_out = response.json()
raw_output = raw_out["choices"][0]["text"]
tool_calls = extract_tool_call_info(raw_output)
if len(tool_calls) == 0:
# Вызовов инструментов нет
msg = raw_output
break
else:
for tool_call in tool_calls:
tool_call_name = tool_call['function']['name']
tool_call_arguments = json.loads(tool_call['function']['arguments'])
tool_function = tool_map[tool_call_name]
tool_result = tool_function(tool_call_arguments)
messages.append({
"role": "tool",
"tool_call_id": tool_call['id'],
"name": tool_call_name,
"content": json.dumps(tool_result),
})
print('-' * 100)
print(msg)

Здесь extract_tool_call_info разбирает вывод модели и возвращает информацию о вызовах. Простая реализация:

def extract_tool_call_info(tool_call_rsp: str):
if '<|tool_calls_section_begin|>' not in tool_call_rsp:
# Вызовов инструментов нет
return []
import re
pattern = r"<\|tool_calls_section_begin\|>(.*?)<\|tool_calls_section_end\|>"
tool_calls_sections = re.findall(pattern, tool_call_rsp, re.DOTALL)
# Извлекаем несколько вызовов инструментов
func_call_pattern = r"<\|tool_call_begin\|>\s*(?P<tool_call_id>[\w\.]+:\d+)\s*<\|tool_call_argument_begin\|>\s*(?P<function_arguments>.*?)\s*<\|tool_call_end\|>"
tool_calls = []
for match in re.findall(func_call_pattern, tool_calls_sections[0], re.DOTALL):
function_id, function_args = match
# function_id: functions.get_weather:0
function_name = function_id.split('.')[1].split(':')[0]
tool_calls.append(
{
"id": function_id,
"type": "function",
"function": {
"name": function_name,
"arguments": function_args
}
}
)
return tool_calls