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 參數。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。