解决 Ollama + LiteLLM + Autogen 中的 'openai.BadRequestError: Invalid role' 错误
2025-03-04 11:13:42
Ollama + LiteLLM + Autogen 使用中出现的 "openai.BadRequestError: Invalid role" 错误解决
问题
我在使用 Ollama 服务和 LiteLLM 代理搭建的环境下,设计了一个 Autogen 群聊。 群聊中包含以下几个角色:
User_Proxy
: 提问SQL_Generator
: 生成 SQL 语句SQL_Runner
: 执行 SQL 语句 (通过工具调用)Result_Validator
: 验证结果
运行时发现两个问题:
- 群聊在
SQL_Runner
返回数据库数据之前都正常。但是,下一步理应由Result_Validator
发言,可实际上SQL_Generator
被指定为下一个发言者。尽管我在系统消息中明确指示SQL_Runner
应将结果传递给Result_Validator
。 - 不知为何,在
SQL_Runner
执行活动后,SQL_Generator
的角色仍然是tool
, 而不是[system, user, assistant]
中的一个,导致了如下错误:
**** * Response from calling tool (call_6493f510-d318-4e85-b552-8963bece5fcb) **** *
[
// data from DB
]
Next speaker: SQL_Generator
// error trace
openai.BadRequestError: Error code: 400 - {'error': {'message': 'invalid role: tool, role must be one of [system, user, assistant]', 'type': 'api_error', 'param': None, 'code': None}}
问题原因分析
问题 1: 发言者选择错误
Autogen 默认的 speaker_selection_method
是 auto
。 在 auto
模式下,Autogen 会尝试根据上下文自动选择下一个发言者,这可能会受到多种因素的影响,包括 LLM 的输出、对话历史等,不一定完全按照你的预期进行。 即使设置成 round_robin
, 也有可能因为中间工具的调用导致顺序错乱.
问题 2: 角色(Role)错误
LiteLLM 在处理工具(tool)调用时,会将 Agent 的角色临时设置为 tool
。在工具调用结束后,理论上 LiteLLM 应该将角色恢复为 assistant
或者 user
。但从报错信息看,LiteLLM 并未正确恢复角色,导致 SQL_Generator
的角色保持为 tool
,从而触发了 OpenAI API 的 invalid role
错误。
解决方案
针对这两个问题,可以尝试以下解决方案:
解决方案 1: 强制指定发言顺序 (针对问题 1)
原理
与其依赖 Autogen 自动选择,不如在代码层面显式控制发言流程, 确保SQL_Runner
执行完毕后, 一定是Result_Validator
接收信息.
代码示例
# ... (之前的代码) ...
# 不再使用 groupchat manager,而是手动控制流程
user_proxy.initiate_chat(sql_generator, message=question) #初始对话,User_Proxy 询问 SQL_Generator
# 获取 SQL_Generator 的回复(SQL 语句)
sql_query = sql_generator.last_message()["content"]
# 将 SQL 语句传递给 SQL_Runner
sql_runner_response = user_proxy.send(recipient=sql_runner, message=sql_query)
sql_result = sql_runner_response.content
# 将 SQL_Runner 的结果传递给 Result_Validator
validator_response = user_proxy.send(recipient=result_validator,message = sql_result)
#处理Result_Validator结果.
# ... (后续处理) ...
代码解释:
这段代码移除了GroupChatManager
, 而是用 user_proxy.send()
方法, 直接指定消息的接收者. 这样, 对话的流程就完全由代码控制了.
- 使用
user_proxy.initiate_chat(sql_generator, message=question)
发起提问给SQL_Generator
. - 通过
sql_generator.last_message()["content"]
拿到上一步生成的SQL 语句. - 调用
user_proxy.send(recipient=sql_runner, message=sql_query)
, 把SQL发给SQL_Runner
. - 通过
user_proxy.send(recipient=result_validator,message = sql_result)
,将SQL执行的结果发送给Result_Validator
处理.
解决方案 2: 手动重置角色 (针对问题 2) & 修改 LiteLLM 配置
原理
在每次SQL_Runner
工具调用完成后, 主动设置下一个发言者(也就是SQL_Generator
)的角色为 assistant
。 由于LiteLLM做了一层代理, 我们需要通过特别设置来告知LiteLLM。
代码示例(针对使用了工具的Agent, 这里是SQL_Runner
):
修改SQL_Runner
的定义, 使用自定义的LLM 配置, 利用LiteLLM的call_with_response_model
特性:
from litellm import completion_with_retries
local_llm_config = {
"config_list": litellm_config_list,
"cache_seed": None,
"custom_llm_provider": "litellm", #显式指出,这样可以启用call_with_response_model
"call_with_response_model": True, #开启litellm特性
}
sql_runner = autogen.AssistantAgent(
name=Role.SQL_RUNNER,
system_message=Prompt.SQL_RUNNER,
llm_config=local_llm_config, # 修正的llm_config
)
# ... 其他Agent定义不变...
操作步骤与解释:
- 修改
local_llm_config
: 添加"custom_llm_provider": "litellm"
和"call_with_response_model": True
这两行, 这会告诉 AutoGen, 使用litellm
作为底层供应商,并且允许在请求时修改模型行为.call_with_response_model
这个字段很重要. - 使用新的配置 :
sql_runner
的llm_config
现在指向了修改后的local_llm_config
。
原理与进阶技巧
-
为什么这样做? Autogen 使用的是标准的
openai
包, 但litellm
做了额外的逻辑. 开启call_with_response_model
后, AutoGen 会使用LiteLLM 特有的方法去发送请求,这个方法处理工具调用的时候,在消息前后会自动添加角色相关的设置. -
如果以上方法还是无效(虽然不太可能), 还可以直接手动去设置
SQL_Generator
的最后一条消息的role. 示例代码(加在把SQL交给SQL_Runner
之后):if sql_generator.chat_messages: for message_list in sql_generator.chat_messages.values(): if len(message_list)>0: message_list[-1]["role"] = "assistant" #强制最后一条消息是assistant
解决方案 3:避免工具调用导致的状态问题(通用方法)
原理
工具调用是引起问题的主要原因。如果我们不用tool
的模式来实现SQL_Runner
, 转而直接让LLM生成包含代码的回复, 并由Autogen去执行,或许能简化这个问题。
代码示例
sql_runner = autogen.AssistantAgent(
name=Role.SQL_RUNNER,
system_message= "你是一个SQL执行专家,根据收到的SQL,给出可以运行的python 代码, 使用```python ... ```包裹起来" + Prompt.SQL_RUNNER, #修改Prompt
llm_config=local_llm_config, # 注意,这里的配置可以使用普通的配置了, 不需要特别指定
)
#不需要再使用register装饰器
# @user_proxy.register_for_execution()
# @sql_runner.register_for_llm(description="SQL query runner")
# def run_sql(sql: Annotated[str, "SQL query string to run"]) -> str:
# with PostgresManager() as db:
# db.get_rds_connection()
# return db.run_sql(sql)
# user_proxy 已经设置了 code_execution_config
# 因此, 如果sql_runner给出代码块,会自动执行。
# 可以继续配合解决方案1,使用 user_proxy.send
代码解释:
- 不再用
register_for_llm
注册成一个工具。 - 修改
SQL_Runner
的system_message
, 让它返回可执行的Python 代码, 而不是直接执行SQL。 user_proxy
那里已经设置了code_execution_config
(你原本的代码就有), 所以只要SQL_Runner
给出代码块,Autogen就会执行。- 然后,继续使用解决方案1中
user_proxy.send()
的手动流程控制, 将SQL执行的结果发送给Result_Validator
.
原理和进阶
这种方法避免了直接使用 tool calling,从而回避了可能出现的角色状态问题. Autogen 内置了代码执行的功能。 这种方式更加简单直接,且减少出错的概率. 我们可以进一步在Prompt 里面细化, 指示LLM应该如果输出结果等,可以更方便控制流程。
安全建议
- 在处理数据库操作时,务必谨慎对待用户输入,防止 SQL 注入攻击。可以使用参数化查询等方式来增强安全性。
- 对 Autogen 的执行环境进行隔离,限制其权限。
总结
问题产生的根本原因在于 LiteLLM 在工具调用后的角色恢复机制存在缺陷,以及 Autogen 在自动模式下发言者选择的不确定性。通过手动控制发言流程以及显式重置角色,或者避开工具调用方式,可以有效解决这些问题. 选择哪种方案,取决于你的实际场景和偏好。 方案3 从根本上规避问题, 可能是最佳的选择。