2025, Nov 09 21:02
Как продолжить незавершённый вызов инструмента в LLM: префиксная допись на Hugging Face Transformers
Разбираем, как дописать незавершённый вызов инструмента в LLM без сброса по EOS: префиксное продолжение внутри реплики, пример на Hugging Face Transformers
Вызов инструментов с помощью LLM обычно опирается на простой двухшаговый сценарий: сначала перечислить варианты, затем выполнить один из них. На практике небольшие модели нередко «зависают» между этими шагами: останавливаются после списка файлов и так и не делают следующий вызов, который действительно открывает документ. Отсюда логичный вопрос: можно ли заставить модель продолжить незавершённую реплику ассистента — дописать начатое на странице, а не собирать ответ заново?
Минимальный пример
Представьте поток «файлового проводника», где ассистент сперва перечисляет файлы, а затем вызывает функцию, чтобы открыть конкретный. В этот момент разрешён только второй вызов инструмента, так что продолжение должно быть очевидным, но модель порой останавливается, не доведя его до конца.
Input:
"""
User: Open the about file.
Assistant: *list_dir()
Tool: Available files, pleases select one:
- file.txt
- example.txt
- about.txt
- random.txt
Assistant: *read_file("
"""
LLM Output:
"""
about.txt")
"""
Желаемое поведение — простая автодопись в рамках той же реплики ассистента.
Что на самом деле ломается
Ответ генерируется токен за токеном, но многие чат-API завершают сообщение ассистента, как только встречают маркер конца последовательности. После этого следующая генерация идёт уже новой репликой, а не продолжением текущей. Если платформа не поддерживает продолжение от префикса внутри текущего сообщения ассистента, нельзя надёжно подтолкнуть модель «подхватить на середине вызова» и дописать уже начатые аргументы функции.
Иными словами, нужна допись по префиксу внутри самого сообщения ассистента. Когда это недоступно, второй вызов инструмента часто пропадает.
Практический обходной путь с Transformers
Когда API не предоставляет такую возможность, можно приблизить её, собрав чат-промпт вручную и удалив токен EOS, чтобы модель продолжила с заданного префикса. Ниже — компактный демонстрационный пример на Hugging Face Transformers и PyTorch. Он формирует вход чата, убирает финальный EOS, а затем шагает токен за токеном, позволяя модели дописать аргументы для незавершённого вызова инструмента.
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
runtime_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(runtime_device)
repo_id = 'Qwen/Qwen3-0.6B'
tok = AutoTokenizer.from_pretrained(repo_id)
lm = AutoModelForCausalLM.from_pretrained(repo_id).to(runtime_device)
def fetch_file(filename: str) -> str:
"""
Открывает текстовый документ с именем "filename".
Пример вызова:
fetch_file("example.txt")
Аргументы:
filename: имя файла документа
Возвращает:
str: содержимое файла
"""
return f"Succesfully opened file \"{filename}\"."
toolset = [fetch_file]
directory_view = """
File Explorer
Available files, use the "*open_file()*" function to open only one:
- about.txt
- coding_paper.txt
- system_requirements.txt
- updates.txt"""
chat_log = [
{'role': 'user', 'content': "What's your latest update?"},
{'role': 'tool', 'content': directory_view},
{'role': 'assistant', 'content': '<tool_call>\n{"name": "fetch_file", "arguments": {"filename": "'}
]
TOPK = 5
MAX_TOKENS_LIMIT = 32768
def continue_span(dialogue):
seq_ids = tok.apply_chat_template(
dialogue,
tools=toolset,
return_tensors='pt',
padding=True,
truncation=True
).to(runtime_device)[:, :-2] # убрать токен EOS
print(tok.decode(seq_ids[0], skip_special_tokens=False), end='', flush=True)
while True:
piece, seq_ids, is_eos = next_symbol(seq_ids)
if is_eos:
break
print(piece, end='', flush=True)
def next_symbol(seq_ids):
with torch.no_grad():
out = lm(seq_ids)
logits = out.logits
last_step = logits[0, -1, :]
probs = torch.softmax(last_step, dim=-1)
topk_probs, topk_indices = torch.topk(probs, TOPK)
best_id = topk_indices[0].reshape(1,)
seq_ids = torch.cat([seq_ids, best_id.unsqueeze(0)], dim=-1)
piece = tok.decode([best_id.item()])
is_eos = False
if best_id == tok.eos_token_id:
is_eos = True
return piece, seq_ids, is_eos
continue_span(chat_log)
Этот фрагмент завершает аргументы вызова инструмента, продолжая частичный вывод ассистента. Ключевой момент — из собранного представления чата удаляется конечный токен, благодаря чему модель воспринимает незакрытый вызов как продолжающуюся последовательность, а не как завершённую реплику.
Почему это важно
Продолжение внутри реплики улучшает единообразие форматирования и снижает риск нестабильного разбора вокруг «каркаса» вызовов инструментов. Когда модель раз за разом не выдаёт второй вызов, возможность возобновить генерацию из середины сообщения сохраняет структуру и избавляет от трюков с перепромптингом, которые всё равно не гарантируют нужный формат.
Выводы
Если нужно принудительно выполнить последующий вызов инструмента из частично написанной реплики, а ваш чат-рантайм этого не умеет, временной мерой может быть самостоятельная сборка промпта и продолжение генерации после пропущенного сегмента. Подход выше — лишь демонстрация того, как дописать вызов инструмента; это не фреймворк для его исполнения. В продакшене следите за возможностями платформы и используйте встроенные функции вроде принудительных вызовов инструментов или инструментов с выбором из нескольких опций по мере их появления.