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"}
# Собираем описания инструментов в toolstools = [{ "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 jsonfrom openai import OpenAImodel_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 = Nonewhile 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 = Nonemsg = ''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 requestsfrom transformers import AutoTokenizermessages = [ {"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