2025, Oct 16 05:18
Как исправить ValueError с agent_scratchpad в LangChain structured chat
Как устранить ValueError в LangChain: agent_scratchpad ожидается строкой в structured chat. Объясняем причину и даем правильный prompt с рабочим кодом и примером.
При подключении структурированного чат-агента в LangChain коварная мелочь может привести к исключению, которое кажется не связанным с вашим кодом: ValueError: variable agent_scratchpad should be a list of base messages, got of type <class 'str'>. Причина в том, как агент ожидает «протягивать» свои промежуточные рассуждения через промпт. Если поместить agent_scratchpad как placeholder сообщений для структурированного чат-агента, он упадёт.
Как воспроизвести проблему
Ниже — минимальная конфигурация: вызывается фиктивная LLM, ей предлагается задействовать простой инструмент и затем вернуть итоговый ответ. Промпт составлен под structured chat-агента, но agent_scratchpad ошибочно передаётся как MessagesPlaceholder.
import asyncio
import json
from langchain.agents import AgentExecutor, create_structured_chat_agent, Tool
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import AIMessage
from langchain_community.chat_models.fake import FakeMessagesListChatModel
# 1. Определяем предсказуемый инструмент
def echo_utility(text: str) -> str:
    print(f"Tool called with input: '{text}'")
    return "The tool says hello back!"
utility_catalog = [
    Tool(
        name="simple_tool",
        func=echo_utility,
        description="A simple test tool.",
    )
]
# 2. Ответы в формате structured chat
mock_outputs = [
    AIMessage(
        content=json.dumps({
            "action": "simple_tool",
            "action_input": {"input": "hello"}
        })
    ),
    AIMessage(
        content=json.dumps({
            "action": "Final Answer",
            "action_input": "The tool call was successful. The tool said: 'The tool says hello back!'"
        })
    ),
]
fake_llm = FakeMessagesListChatModel(responses=mock_outputs)
# 3. Промпт с некорректным размещением agent_scratchpad
broken_prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        """Respond to the human as helpfully and accurately as possible. You have access to the following tools:
{tools}
Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input).
Valid "action" values: "Final Answer" or {tool_names}
Provide only ONE action per $JSON_BLOB, as shown:
{{
  "action": $TOOL_NAME,
  "action_input": $INPUT
}}
Follow this format:
Question: input question to answer
Thought: consider previous and subsequent steps
Action:
{{
$JSON_BLOB
}}
Observation: action result
... (repeat Thought/Action/Observation as needed)
Thought: I know what to respond
Action:
{{
  "action": "Final Answer",
  "action_input": "Final response to human"
}}
Begin! Reminder to ALWAYS respond with a valid json blob of a single action. Use tools if necessary. Respond directly if appropriate. Format is Action:```$JSON_BLOB```then Observation"""
    ),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])
# 4. Агент и исполнитель (executor)
structured_agent = create_structured_chat_agent(fake_llm, utility_catalog, broken_prompt)
runner = AgentExecutor(
    agent=structured_agent,
    tools=utility_catalog,
    verbose=True,
    handle_parsing_errors=True,
    max_iterations=3,
)
# 5. Вызов
result = asyncio.run(runner.ainvoke({"input": "call the tool"}))
Задействованные зависимости: langchain==0.3.27, langchain-community==0.3.27, langchain-core==0.3.74, langchain-aws==0.2.30, langchain-openai==0.3.29. Версия Python: 3.9.
Что именно идёт не так
AgentExecutor запускает цикл, где каждая итерация опирается на результат предыдущего шага. Разные реализации агентов по‑разному добавляют эти промежуточные шаги. Одни преобразуют их в сообщения и расширяют диалог этим списком сообщений. Другие склеивают промежуточные шаги в строку и подклеивают её к пользовательскому промпту. Структурированный чат-агент относится ко второй группе: он ожидает, что agent_scratchpad будет внедрён в пользовательское сообщение как строка, а не как список сообщений. Если передать MessagesPlaceholder для agent_scratchpad, исполнитель попытается обработать строку как список базовых сообщений, что и приводит к ValueError.
Это различие задокументировано для structured chat-агента. На практике это значит, что для этого агента нужно размещать agent_scratchpad прямо в human‑сообщении. Напротив, агенты, опирающиеся на сообщение‑ориентированные промежуточные шаги, используют MessagesPlaceholder.
Конкретно: structured_chat_agent, react_agent, self_ask_with_search_agent, стандартный sql_agent (когда agent_type не указан) и xml_agent ожидают, что agent_scratchpad будет частью пользовательского промпта в виде строки. А вот json_chat_agent, openai_tools_agent, sql_agent при agent_type, установленном в "tool-calling", и tool_calling_agent ожидают, что agent_scratchpad будет MessagesPlaceholder.
Исправление
Измените промпт так, чтобы agent_scratchpad был частью сообщения пользователя, а не placeholder'ом сообщений. Ничего больше в управляющей логике или настройке инструментов менять не требуется.
import asyncio
import json
from langchain.agents import AgentExecutor, create_structured_chat_agent, Tool
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import AIMessage
from langchain_community.chat_models.fake import FakeMessagesListChatModel
# 1. Определение инструмента остаётся прежним
def echo_utility(text: str) -> str:
    print(f"Tool called with input: '{text}'")
    return "The tool says hello back!"
utility_catalog = [
    Tool(
        name="simple_tool",
        func=echo_utility,
        description="A simple test tool.",
    )
]
# 2. Заглушки ответов LLM без изменений
mock_outputs = [
    AIMessage(
        content=json.dumps({
            "action": "simple_tool",
            "action_input": {"input": "hello"}
        })
    ),
    AIMessage(
        content=json.dumps({
            "action": "Final Answer",
            "action_input": "The tool call was successful. The tool said: 'The tool says hello back!'"
        })
    ),
]
fake_llm = FakeMessagesListChatModel(responses=mock_outputs)
# 3. Верное размещение: agent_scratchpad добавляется к пользовательскому промпту
fixed_prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        """Respond to the human as helpfully and accurately as possible. You have access to the following tools:
{tools}
Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input).
Valid "action" values: "Final Answer" or {tool_names}
Provide only ONE action per $JSON_BLOB, as shown:
{{
  "action": $TOOL_NAME,
  "action_input": $INPUT
}}
Follow this format:
Question: input question to answer
Thought: consider previous and subsequent steps
Action:
{{
$JSON_BLOB
}}
Observation: action result
... (repeat Thought/Action/Observation as needed)
Thought: I know what to respond
Action:
{{
  "action": "Final Answer",
  "action_input": "Final response to human"
}}
Begin! Reminder to ALWAYS respond with a valid json blob of a single action. Use tools if necessary. Respond directly if appropriate. Format is Action:```$JSON_BLOB```then Observation"""
    ),
    (
        "human",
        "{input}\n{agent_scratchpad}"
    ),
])
structured_agent = create_structured_chat_agent(fake_llm, utility_catalog, fixed_prompt)
runner = AgentExecutor(
    agent=structured_agent,
    tools=utility_catalog,
    verbose=True,
    handle_parsing_errors=True,
    max_iterations=3,
)
result = asyncio.run(runner.ainvoke({"input": "call the tool"}))
Почему это важно
AgentExecutor собирает промпты итеративно. Если дизайн агента предполагает, что промежуточные шаги конкатенируются в пользовательское сообщение, то placeholder сообщений нарушает это ожидание, и исполнитель не может согласовать типы. Понимание, какие агенты добавляют сообщения, а какие объединяют строки, избавляет от хрупкой проводки промпта, экономит время на непонятных ошибках типов и делает циклы вызова инструментов предсказуемыми.
Практические выводы
Для structured chat-агента размещайте agent_scratchpad внутри пользовательского сообщения. Если переключаетесь на агентов, которые расширяют список сообщений, перенесите agent_scratchpad в MessagesPlaceholder. Если столкнулись с описанной ошибкой, сначала проверьте проводку промпта — обычно достаточно одной правки в строке.
Статья основана на вопросе на StackOverflow от hitesh и ответе от cottontail.