baidubce / bce-qianfan-sdk

Provide best practices for LMOps, as well as elegant and convenient access to the features of the Qianfan MaaS Platform. (提供大模型工具链最佳实践,以及优雅且便捷地访问千帆大模型平台)
https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html
Apache License 2.0
330 stars 49 forks source link

LangGraph 使用工具调用后 `astream_log` 无法忽略工具调用消息为空的问题 #854

Closed LogCreative closed 2 days ago

LogCreative commented 2 days ago

System Info

qianfan version

System Version

Reproduction

继续 #842:

考虑到不再支持老式的 AgentExecutor 用法,在尝试使用 LangGraph 进行工具调用时依然出现了一些问题,修改 LangGraph 官方示例 使得调用 astream_log 函数输出结果:

import os

from langchain_community.chat_models import QianfanChatEndpoint
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langgraph.prebuilt import ToolNode
from langgraph.prebuilt import create_react_agent

import asyncio

@tool
def get_weather(location: str):
    """Call to get the current weather."""
    if location.lower() in ["sf", "san francisco"]:
        return "It's 60 degrees and foggy."
    else:
        return "It's 90 degrees and sunny."

@tool
def get_coolest_cities():
    """Get a list of coolest cities"""
    return "nyc, sf"

tools = [get_weather, get_coolest_cities]
tool_node = ToolNode(tools)

os.environ["QIANFAN_AK"] = "*"  # 或者在环境变量中设置
os.environ["QIANFAN_SK"] = "*"  # 或者在环境变量中设置

model = QianfanChatEndpoint(
            streaming=False,
            model="ernie-4.0-8k-preview",
            **{"top_p": 0.4, "temperature": 0.1, "penalty_score": 1},
        )

graph = create_react_agent(model, tools=tools)

async def main():
    async for s in graph.astream_log({"messages": [HumanMessage("what's the weather in sf?")]}): # 注意此处
        print(s)

asyncio.run(main())

就会报错:

  File "python3.11/site-packages/langchain_community/chat_models/baidu_qianfan_endpoint.py", line 662, in _astream
    async for res in await self.client.ado(**params):
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "python3.11/site-packages/qianfan/resources/llm/chat_completion.py", line 1947, in ado
    return await impl.ado(
           ^^^^^^^^^^^^^^^
  File "python3.11/site-packages/qianfan/resources/llm/chat_completion.py", line 1468, in ado
    resp = await self._ado(
           ^^^^^^^^^^^^^^^^
  File "python3.11/site-packages/qianfan/resources/llm/base.py", line 620, in _ado
    return await self._arequest(model, stream, retry_config, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "python3.11/site-packages/qianfan/resources/llm/base.py", line 783, in _arequest
    raise e
  File "python3.11/site-packages/qianfan/resources/llm/base.py", line 763, in _arequest
    resp = await self._client.async_llm(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "python3.11/site-packages/qianfan/resources/requestor/openapi_requestor.py", line 624, in async_llm
    return await self._async_with_retry(retry_config, _helper)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "python3.11/site-packages/qianfan/resources/requestor/base.py", line 535, in _async_with_retry
    return await _retry_wrapper(*args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "python3.11/site-packages/tenacity/asyncio/__init__.py", line 189, in async_wrapped
    return await copy(fn, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "python3.11/site-packages/tenacity/asyncio/__init__.py", line 111, in __call__
    do = await self.iter(retry_state=retry_state)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "python3.11/site-packages/tenacity/asyncio/__init__.py", line 153, in iter
    result = await action(retry_state)
             ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "python3.11/site-packages/tenacity/_utils.py", line 99, in inner
    return call(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "python3.11/site-packages/tenacity/__init__.py", line 398, in <lambda>
    self._add_action_func(lambda rs: rs.outcome.result())
                                     ^^^^^^^^^^^^^^^^^^^
  File "python3.11/concurrent/futures/_base.py", line 449, in result
    return self.__get_result()
           ^^^^^^^^^^^^^^^^^^^
  File "python3.11/concurrent/futures/_base.py", line 401, in __get_result
    raise self._exception
  File "python3.11/site-packages/tenacity/asyncio/__init__.py", line 114, in __call__
    result = await fn(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "python3.11/site-packages/qianfan/resources/requestor/base.py", line 533, in _retry_wrapper
    return await func(*args)
           ^^^^^^^^^^^^^^^^^
  File "python3.11/site-packages/qianfan/resources/requestor/openapi_requestor.py", line 204, in retry_wrapper
    return await func(*args)
           ^^^^^^^^^^^^^^^^^
  File "python3.11/site-packages/qianfan/resources/requestor/openapi_requestor.py", line 608, in _helper
    await self._async_request_stream(
  File "python3.11/site-packages/qianfan/resources/requestor/base.py", line 307, in wrapper
    resp = await func(requestor, request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "python3.11/site-packages/qianfan/resources/requestor/openapi_requestor.py", line 243, in _async_request_stream
    raise e
  File "python3.11/site-packages/qianfan/resources/requestor/openapi_requestor.py", line 236, in _async_request_stream
    self._check_error(json.loads(body))
  File "python3.11/site-packages/qianfan/resources/requestor/openapi_requestor.py", line 319, in _check_error
    raise errors.APIError(error_code, err_msg, req_id)
qianfan.errors.APIError: api return error, req_id: as-q7etfe1s8d code: 336003, msg: message content can not be empty

而如果换成 astream 就没问题:

async def main():
    async for s in graph.astream({"messages": ["what's the weather in sf?"]}):  # 注意此处
        print(s)

import asyncio
asyncio.run(main())

初步 debug 发现,在使用 astream_log 时没有对含有 tool_call 的那一个 AIMessage 做特殊处理(该 message 的 content 应该为空),导致 ToolMessage 产生再输入大模型时:[HumanMessage, AIMessage(content=''), ToolMessage] 就没有被校验通过。而使用 astream 函数就没有这个问题。

由于 astream_log 是 LangServe Playground 中使用的函数,希望能够给出一种解决方案。

LogCreative commented 2 days ago

create_react_agent 展开可以用下面的方式绕过 astream_log 未进行的判断,但也会导致后续推理结果的偏差:

import os

from langchain_community.chat_models import QianfanChatEndpoint
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langgraph.prebuilt import ToolNode

@tool
def get_weather(location: str):
    """Call to get the current weather."""
    if location.lower() in ["sf", "san francisco"]:
        return "It's 60 degrees and foggy."
    else:
        return "It's 90 degrees and sunny."

@tool
def get_coolest_cities():
    """Get a list of coolest cities"""
    return "nyc, sf"

tools = [get_weather, get_coolest_cities]
tool_node = ToolNode(tools)

os.environ["QIANFAN_AK"] = "*"
os.environ["QIANFAN_SK"] = "*"

model = QianfanChatEndpoint(
            streaming=False,
            model="ernie-4.0-8k-preview",
            **{"top_p": 0.4, "temperature": 0.1, "penalty_score": 1},
        )

from langgraph.graph import StateGraph, MessagesState, START, END

model_with_tools = model.bind_tools(tools)

def should_continue(state: MessagesState):
    messages = state["messages"]
    last_message = messages[-1]
    if last_message.tool_calls:
        state["messages"][-1].content = "使用工具调用" # 绕过判断
        return "tools"
    return END

def call_model(state: MessagesState):
    messages = state["messages"]
    response = model_with_tools.invoke(messages)
    return {"messages": [response]}

workflow = StateGraph(MessagesState)

# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)

workflow.add_edge(START, "agent")
workflow.add_conditional_edges("agent", should_continue, ["tools", END])
workflow.add_edge("tools", "agent")

graph = workflow.compile()

async def main():
    async for s in graph.astream_log({"messages": [HumanMessage("what's the weather in sf?")]}):
        print(s)

import asyncio
asyncio.run(main())

但是如果此时再改用 astream 就会弹出如下的错误:

qianfan.errors.APIError: api return error, req_id: as-jmwha8b6j4 code: 336003, msg: Message format error, index [2] message content and function_call is mutually exclusive, only one must be null
Dobiichi-Origami commented 2 days ago

感谢您的反馈,在较旧版本的 Langchain 中并未出现上述问题。我们这边会对新版本中引入的这个问题进行排查 :)

Dobiichi-Origami commented 2 days ago

修复 PR 已提 @LogCreative