Sunforger

Sunforger

第一次跑通 langchain + ollama 的一点踩坑笔记

版本与配置#

软件版本:

langchain                 0.3.25                   pypi_0    pypi
langchain-community       0.3.25                   pypi_0    pypi
langchain-core            0.3.65                   pypi_0    pypi
langchain-ollama          0.3.3                    pypi_0    pypi
langchain-openai          0.3.24                   pypi_0    pypi
langchain-text-splitters  0.3.8                    pypi_0    pypi

python 版本:3.10.16

需求#

需要进行一个 episodic 的任务。在每一个 step 里,首先以传统方法求出一个方案。然后再过大语言模型对方案进行优化,以观察最后的效果。

实现#

文件架构

src/llm
├── agents.py
├── keys.toml
├── models.py
├── prompts
│   ├── __init__.py
│   ├── [xxxx] // your scenario name
│   │   ├── human.txt
│   │   └── system.txt
│   └── utils.py
└── tools.py

先在本地 Ollama 上使用一下,通了之后再去考虑对接强大一点的模型。比如我这面首先使用了 qwen2:7b 这个模型

# test/test_llm.py
from langchain.agents import create_structured_chat_agent

from src.llm.models import get_model
from src.llm.prompts.utils import load_prompts

def test_llm():
    model = get_model("ollama", "qwen2:7b")
    prompt = load_prompts(scenario="routing")

    agent = create_structured_chat_agent(model, [], prompt)
    result = agent.invoke({"input": "hello", "intermediate_steps": []})
    print(result)
# models.py
from langchain_ollama import ChatOllama
from langchain_openai import ChatOpenAI

def get_model(provider: str, model: str, temperature: float = 0.3):
    if provider == "ollama":
        return ChatOllama(
            model=model, temperature=temperature, base_url="http://localhost:11434" 
        ) # 传参注意,llm 要传到 model 参数里去,否则会报 Validation Error
    elif provider == "openai":
        return ChatOpenAI(...) # 省略
    else:
        raise ValueError("unsupported provider")
# prompts/utils.py
from pathlib import Path

from langchain.prompts import (
    ChatPromptTemplate,
    HumanMessagePromptTemplate,
    SystemMessagePromptTemplate,
)

def load_prompts(scenario: str):
    path = Path(__file__).parent / scenario

    with open(path / "system.txt", "r", encoding="utf-8") as f:
        s_template = f.read()
        s_prompt = SystemMessagePromptTemplate.from_template(s_template)

    with open(path / "human.txt", "r", encoding="utf-8") as f:
        h_template = f.read()
        h_prompt = HumanMessagePromptTemplate.from_template(h_template)

    return ChatPromptTemplate.from_messages([s_prompt, h_prompt])

提示词文件,参考链接配制 https://smith.langchain.com/hub/hwchase17/structured-chat-agent

# human.txt

{input}

{agent_scratchpad}
(reminder to respond in a JSON blob no matter what)
# system.txt

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 N times)

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

坑点#

1. 版本#

langchain 还在不断迭代,早先版本和现今版本有一定的接口差异,需要特别注意。在网络查询资料时,时效性很重要。

2. 提示词#

在使用 create_structured_chat_agent 函数创建 agent 的时候,要注意提示词内必须包含 tools, tools_name, agent_scratchpad 这三个占位符,并用 {} 包裹起来。

否则报错 ValueError: Prompt missing required variables: {'agent_scratchpad', 'tool_names', 'tools'}

在解析时,这个提示词模板会被认为是 f-string 所以会自动将 {} 中的内容替换成变量值

更新 25/6/24#

用传统 .txt 作为提示词,扩展性太差了(因为不支持注释)。选用 jinja2 模板来管理提示词。

这样既能支持注释,又可以解析占位符。

jinja 模板使用 {{...}} 来做占位符,这与 langchain 中使用 {...} 作为提示词占位符不同。这两个不能混用,否则会发生解析错误。

对于 system prompt,通常 agent 初始化的时候就要给出,然后后续不会更改。如果需要传参,建议通过 jinja 模板传入。

from langchain_core.prompts.string import jinja2_formatter

s_template = jinja2_formatter(
  Path(path / "system.jinja").read_text(encoding="utf-8"),
  ... # 传入占位符对应的参数  
)
s_prompt = SystemMessagePromptTemplate.from_template(s_template)

对于 human prompt 也就是 agent 的输入,通常是每一步实时更新的,所以,可以采用 jinja 载入,也可以通过 .invoke() 方法传入。但需要注意,前者需要将占位符写成 {{...}} 形式,然后以类似 system prompt 的方式导入 human prompt。而后者需要注意,应该以 langchain 风格的 {...} 作为占位符,然后直接以 string 的方式导入(如下所示)。

h_template = Path(path / "human.jinja").read_text(encoding="utf-8")
h_prompt = HumanMessagePromptTemplate.from_template(h_template)

3. 调用#

在执行 agent.invoke() 的时候。需要给出 input 字段内容的同时,还需要给出 intermediate_steps 字段的值。

否则报错 KeyError: 'intermediate_steps'

更新#

可以使用 AgentExecutor 将 agent 包装一下,这样就可以有更精细的定制,并且能够返回调试信息。

## original code
agent = create_structured_chat_agent(model, [], prompt)
result = agent.invoke({"input": "hello", "intermediate_steps": []})

## updated code
agent = create_structured_chat_agent(model, [], prompt)
executor = AgentExecutor.from_agent_and_tools(
    agent=agent, tools=[], verbose=True, return_intermediate_steps=True
)
result = executor.invoke({"input": "hello"})

在允许更精细定制化的同时,也无需手动传入 intermediate_steps 参数。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。