返回

解决 Ollama + LiteLLM + Autogen 中的 'openai.BadRequestError: Invalid role' 错误

Ai

Ollama + LiteLLM + Autogen 使用中出现的 "openai.BadRequestError: Invalid role" 错误解决

问题

我在使用 Ollama 服务和 LiteLLM 代理搭建的环境下,设计了一个 Autogen 群聊。 群聊中包含以下几个角色:

  • User_Proxy: 提问
  • SQL_Generator: 生成 SQL 语句
  • SQL_Runner: 执行 SQL 语句 (通过工具调用)
  • Result_Validator: 验证结果

运行时发现两个问题:

  1. 群聊在 SQL_Runner 返回数据库数据之前都正常。但是,下一步理应由 Result_Validator 发言,可实际上 SQL_Generator 被指定为下一个发言者。尽管我在系统消息中明确指示 SQL_Runner 应将结果传递给 Result_Validator
  2. 不知为何,在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_methodauto。 在 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()方法, 直接指定消息的接收者. 这样, 对话的流程就完全由代码控制了.

  1. 使用user_proxy.initiate_chat(sql_generator, message=question) 发起提问给SQL_Generator.
  2. 通过sql_generator.last_message()["content"]拿到上一步生成的SQL 语句.
  3. 调用user_proxy.send(recipient=sql_runner, message=sql_query), 把SQL发给SQL_Runner.
  4. 通过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定义不变...

操作步骤与解释:
  1. 修改 local_llm_config : 添加 "custom_llm_provider": "litellm""call_with_response_model": True 这两行, 这会告诉 AutoGen, 使用 litellm作为底层供应商,并且允许在请求时修改模型行为. call_with_response_model 这个字段很重要.
  2. 使用新的配置 : sql_runnerllm_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

代码解释:

  1. 不再用register_for_llm注册成一个工具。
  2. 修改SQL_Runnersystem_message, 让它返回可执行的Python 代码, 而不是直接执行SQL。
  3. user_proxy那里已经设置了 code_execution_config (你原本的代码就有), 所以只要SQL_Runner给出代码块,Autogen就会执行。
  4. 然后,继续使用解决方案1中 user_proxy.send() 的手动流程控制, 将SQL执行的结果发送给Result_Validator.

原理和进阶
这种方法避免了直接使用 tool calling,从而回避了可能出现的角色状态问题. Autogen 内置了代码执行的功能。 这种方式更加简单直接,且减少出错的概率. 我们可以进一步在Prompt 里面细化, 指示LLM应该如果输出结果等,可以更方便控制流程。

安全建议

  • 在处理数据库操作时,务必谨慎对待用户输入,防止 SQL 注入攻击。可以使用参数化查询等方式来增强安全性。
  • 对 Autogen 的执行环境进行隔离,限制其权限。

总结

问题产生的根本原因在于 LiteLLM 在工具调用后的角色恢复机制存在缺陷,以及 Autogen 在自动模式下发言者选择的不确定性。通过手动控制发言流程以及显式重置角色,或者避开工具调用方式,可以有效解决这些问题. 选择哪种方案,取决于你的实际场景和偏好。 方案3 从根本上规避问题, 可能是最佳的选择。