上一篇文章我們看了 LCEL 的運作邏輯,也看了 Runnable 如何使用,還看了自定義 Runnable 函數的方式。那今天的實作就是要自定義 Runnable 函數的方式,加上 LangChain 原生的 Runnable 來試著完成!
Output Parser 就是字面上的意思:輸出解析器。一般 AI 回傳的內容都只會是字串形式,那如果我們希望回傳的型態是我們想要的,那就可以使用到 Output Parser。LangChain 官網的 OutputParser 說明頁 可以看到所有可以解析成的型態,比較常見的就是 CSV, JSON 和 Structured,也可以根據自己的需求自定義 Parser,那接下來就來一一實作!
from langchain_ffm import ChatFormosaFoundationModel
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
import pprint
# 選擇模型
llm = ChatFormosaFoundationModel(model="ffm-llama3-70b-chat", temperature=0.01)
# 定義 Json 的資料結構
class Player(BaseModel):
name: str = Field(description="Name of the player")
team: str = Field(description="Team of the player")
position: str = Field(description="Position of the player")
# 將定義的結構傳入 Json Parser
parser = JsonOutputParser(pydantic_object=Player)
# 在 template 的部分設定兩個變數,一個是我們要 input 的變數,另一個是 Json parser 結構的描述
prompt = PromptTemplate(
template = "List three active players in {league}, {format_instructions}",
input_variables = ["league"],
partial_variables = {"format_instructions": parser.get_format_instructions()}
)
# 將這些部分 Chain 起來
chain = prompt | llm | parser
# pprint 可以幫助我們更清楚的看輸出結果的結構
pprint.pprint(chain.invoke("MLB"))
程式碼結果探討 🧐:
print(parser.get_format_instructions())
input_variables
是我們輸入的內容,partial_variables
是上圖 Json format 的 few-shot,才可以精準的讓 AI 知道我們要什麼形式的資料。pprint
有助於幫我看清楚資料的架構,尤其是在 Json 或 dict 型態的資料。from langchain_ffm import ChatFormosaFoundationModel
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field, validator
from typing import List
# 選擇模型
llm = ChatFormosaFoundationModel(model="ffm-llama3-70b-chat", temperature=0.01)
# 定義資料結構
class Kpop(BaseModel):
singer: str = Field(description="Name of the singer")
gender: str = Field(description="Gender of the singer")
group: str = Field(description="Group of the singer")
age: int = Field(description="Age of the singer")
song : List[str] = Field(description="Song of the singer")
# 可以透過 Pydantic 自訂簡單的邏輯驗證
@validator("gender")
def validate_gender(cls, field):
if field not in ["Male", "Female"]:
raise ValueError("Invalid gender value")
return field
# 將定義的結構傳入 Pydantic Parser
parser = PydanticOutputParser(pydantic_object=Kpop)
# 在 template 的部分設定兩個變數,一個是我們要 input 的變數,另一個是 Pydantic parser 結構的描述
prompt = PromptTemplate(
template="Tell me a girl Kpop idols from {company}, {format_instructions}",
input_variables=["company"],
partial_variables={"format_instructions": parser.get_format_instructions()}
)
# 將這些部分 Chain 起來,並顯示結果
chain = prompt | llm | parser
print(chain.invoke("SM Entertainment"))
程式碼結果探討 🧐:
Kpop
的物件。要自定義 Parser 可以透過我們昨天使用過的 RunnableLambda 來幫助我們 Chain起來。
from langchain_ffm import ChatFormosaFoundationModel
from langchain_core.runnables import RunnableLambda
# 選擇模型
llm = ChatFormosaFoundationModel(model="ffm-llama3-70b-chat", temperature=0.01)
# 自定義 Parser 的行為
def parse(message) -> str:
return message.content.upper()
# 將這些部分 Chain 起來,並顯示結果
chain = llm | RunnableLambda(parse)
print(chain.invoke("Who is Aespa?"))
程式碼結果探討 🧐:
先說明一下我打算請 AI 幫我寫一個 to-do list 的網頁,要包含完整的新增修改刪除的 js 程式碼。然後我可以直接這個程式碼轉成網頁輸出,所以先來看看請 AI 輸出 html 程式結果會如何~
from langchain_ffm import ChatFormosaFoundationModel
llm = ChatFormosaFoundationModel(model="ffm-llama3-70b-chat", temperature=0.01, max_new_tokens=5000)
response = llm.invoke("請用html寫一個 to-do list 的網頁給我,要包含新增修改刪除功能,包含js的部分也要幫我寫進去!")
print(response.content)
from langchain_ffm import ChatFormosaFoundationModel
from langchain_core.runnables import RunnableLambda
from langchain_core.prompts import PromptTemplate
import os
# 定義 Output Parser 函數,取出 html 程式的部分
def custom_parser(message) -> str:
html_code = message.content.split("```")[1].replace("html", "", 1)
return html_code
# 將 html 程式放進 flask 執行
def run_flask_app(html_code):
# 如果不存在 templates 資料夾的話,會創建一個
os.makedirs("templates", exist_ok=True)
# 將解析完成的 html 程式寫入 index.html
with open("templates/index.html", "w") as index_file:
index_file.write(html_code)
# 寫一個 flask 的程式
app_code = f"""from flask import Flask, make_response, render_template
app = Flask(__name__)
@app.route('/')
def index():
response = make_response(render_template("index.html"))
return response
if __name__ == '__main__':
app.run()
"""
# 將上面 flask 程式寫入 app.py
with open("app.py", "w") as file:
file.write(app_code)
# 執行 app.py
os.system("python app.py")
# 選擇模型
llm = ChatFormosaFoundationModel(model="ffm-llama3-70b-chat", temperature=0.01, max_new_tokens=5000)
# 指令
template = "請用 html 寫一個 {web} 的網頁給我,要包含{function}功能,包含js的部分也要幫我寫進去!"
prompt = PromptTemplate.from_template(template)
# 將所有 Runnable Chain 起來,並且傳入我要的網頁型態和功能
chain = prompt | llm | RunnableLambda(custom_parser) | RunnableLambda(run_flask_app)
chain.invoke({"web":"to-do list", "function":"新增修改刪除"})
程式碼結果探討 🧐:
render_template
函數是要讀取同路徑中 templates
資料夾中的 html 檔,所以確認如果沒有的話要建立一個資料夾。Flask 程式的部分必須全部貼齊左邊,不然寫進 app.py
之後會大跑版。最後就是透過 os 來執行我們寫入的 app.py 檔,就可以啟動這個網頁了。今天的實戰成功!第一部分 Output Parser 的地方,如果覺得用起來很麻煩的話,不妨嘗試移除 Parser 的部分,然後進行結果的比對,應該就會理解為何他有存在的必要。第二部分將 LangChain 原生的還有自定義的都 Chain 起來了,未來希望可以越 Chain 越多啦,不僅精簡也易於擴充或修改。
已經累積 15 天了~祝其他鐵人賽的參賽者大神朋友們都可以順利完成 30 天挑戰!