Skip to content

自定义数据不支持单独的tool #9843

Description

@simpleNAVI

Checklist / 检查清单

  • I have searched existing issues, and this is a new bug report. / 我已经搜索过现有的 issues,确认这是一个新的 bug report。

Bug Description / Bug 描述

我们目前需要将一些“环境信息”反馈给模型。

  • 如果将该信息放入 user 角色,容易引起模型指令混淆;
  • 如果放 system 角色,不利于多轮对话维护。

因此,这类背景信息最适合放入 tool 角色中,作为客观观测结果(Observation)提供给模型。
但在实际使用中,如果给定的messages包含单独的 role: "tool" 消息,但在其之前没有对应的 assistant 工具调用消息,会直接报错。

事实上,Qwen、GLM 等主流模型的底层模板(Chat Template)均原生支持解析无前置调用的独立 tool 消息(会自动将其转换为 <tool_response> 或 <|observation|> 结构块渲染给模型)。

Image Image Image

How to Reproduce / 如何复现

import json
from swift import get_processor, get_template

准备工具返回结果

tr_sh = json.dumps(dict(city='上海', aqi='72', unit='fahrenheit'), ensure_ascii=False)

tool = {
'type': 'function',
'function': {
'name': 'realtime_aqi',
'description': '天气预报。获取实时空气质量。',
'parameters': {
'type': 'object',
'properties': {'city': {'type': 'string', 'description': '城市名'}},
'required': ['city'],
},
},
}

data = {
'tools': [tool],
'messages': [
{'role': 'user', 'content': '上海今天的天气情况'},
{'role': 'tool', 'content': '这是一个环境信息'},
# 1. 中间过程:模型发起的工具调用(必须是 assistant 角色,且携带 tool_calls 字段)
{
'role': 'assistant',
'tool_calls': [
{
'type': 'function',
'function': {
'name': 'realtime_aqi',
'arguments': {"city": "上海"}
}
}
]
},

    # 2. 中间过程:外部环境/系统返回的工具结果(角色名称必须是 'tool')
    {'role': 'tool', 'name': 'realtime_aqi', 'content': tr_sh},
    
    # 3. 最终解答
    {'role': 'assistant', 'content': '北京AQI为10,良好;上海AQI为72,轻度污染。'},
],

}

template = get_template(
get_processor('Qwen3-8B'),
agent_template='react_en',
add_non_thinking_prefix=False,
)
template.set_mode('train')
inputs = template.encode(data)

print(template.safe_decode(inputs['labels']))
print(inputs['loss_scale'])

报错:
KeyError: 'content'

KeyError Traceback (most recent call last)
Cell In[6], line 95
91 agent_template='react_en',
92 add_non_thinking_prefix=False,
93 )
94 template.set_mode('train')
---> 95 encoded = template.encode(data)
96
97 print('[INPUT_IDS]')
98 print(template.safe_decode(encoded['input_ids']))

File /usr/local/lib/python3.12/dist-packages/torch/utils/_contextlib.py:124, in context_decorator..decorate_context(*args, **kwargs)
120 @functools.wraps(func)
121 def decorate_context(*args, **kwargs):
122 # pyrefly: ignore [bad-context-manager]
123 with ctx_factory():
--> 124 return func(*args, **kwargs)

File /usr/local/lib/python3.12/dist-packages/swift/utils/utils.py:445, in retry_decorator.._retry..new_func(*args, **kwargs)
443 while True:
444 try:
--> 445 return func(*args, **kwargs)
446 except Exception:
447 if i == retry:

File /usr/local/lib/python3.12/dist-packages/swift/template/base.py:615, in Template.encode(self, inputs, return_template_inputs, return_length)
612 inputs = asdict(inputs)
614 if isinstance(inputs, dict):
--> 615 inputs = TemplateInputs.from_dict(inputs)
616 elif isinstance(inputs, TemplateInputs):
617 inputs = deepcopy(inputs)

File /usr/local/lib/python3.12/dist-packages/swift/template/template_inputs.py:219, in TemplateInputs.from_dict(cls, inputs)
216 if chosen_v is not None and rejected_v is None:
217 rejected[k] = chosen_v
--> 219 return cls(**kwargs)

File :7, in create_fn..init(self, chosen, rejected, positive, negative)

File /usr/local/lib/python3.12/dist-packages/swift/template/template_inputs.py:152, in TemplateInputs.post_init(self)
150 continue
151 if key in {'chosen', 'rejected'}:
--> 152 setattr(self, key, StdTemplateInputs.from_dict(value_dict))
153 else:
154 res = []

File /usr/local/lib/python3.12/dist-packages/swift/template/template_inputs.py:82, in StdTemplateInputs.from_dict(cls, inputs)
79 if message['role'] in {'tool_call', 'tool'} and not isinstance(message['content'], str):
80 message['content'] = json.dumps(message['content'], ensure_ascii=False)
---> 82 media_kwargs = StdTemplateInputs.remove_messages_media(messages)
83 for k in list(media_kwargs.keys()):
84 mm_data = media_kwargs[k]

File /usr/local/lib/python3.12/dist-packages/swift/template/template_inputs.py:111, in StdTemplateInputs.remove_messages_media(messages)
109 res = {'images': [], 'audios': [], 'videos': []}
110 for message in messages:
--> 111 content = message['content']
112 if isinstance(content, str):
113 continue

KeyError: 'content'

Additional Information / 补充信息

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions