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.