iT邦幫忙

2024 iThome 鐵人賽

DAY 15
0
生成式 AI

T 大使 AI 之旅系列 第 15

【Day 15】LCEL 結合自訂 & 原生 Runnable 實戰

  • 分享至 

  • xImage
  •  

前情提要

上一篇文章我們看了 LCEL 的運作邏輯,也看了 Runnable 如何使用,還看了自定義 Runnable 函數的方式。那今天的實作就是要自定義 Runnable 函數的方式,加上 LangChain 原生的 Runnable 來試著完成!
https://ithelp.ithome.com.tw/upload/images/20240819/20168336KegHsKd0MB.png

Output Parser

Output Parser 就是字面上的意思:輸出解析器。一般 AI 回傳的內容都只會是字串形式,那如果我們希望回傳的型態是我們想要的,那就可以使用到 Output Parser。LangChain 官網的 OutputParser 說明頁 可以看到所有可以解析成的型態,比較常見的就是 CSV, JSON 和 Structured,也可以根據自己的需求自定義 Parser,那接下來就來一一實作!

JSON 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"))

https://ithelp.ithome.com.tw/upload/images/20240819/20168336L35nJPS0qt.png
程式碼結果探討 🧐:

  • 我們將我們定義的 Json 資料結構傳入 Json Parser 接著再傳入 format instructions,那我們來看看傳入的內容到底是什麼。下圖可以看到其實就是針對 Json 資料結構的描述,並給他 few-shot (一些提示),讓模型輸出的結果可以符合我們要的 Json 格式。
print(parser.get_format_instructions())

https://ithelp.ithome.com.tw/upload/images/20240819/20168336AWbctS6hkV.png

  • 在傳入變數的部分 input_variables 是我們輸入的內容,partial_variables 是上圖 Json format 的 few-shot,才可以精準的讓 AI 知道我們要什麼形式的資料。
  • pprint 有助於幫我看清楚資料的架構,尤其是在 Json 或 dict 型態的資料。

Structured Parser 實戰🔥

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"))

https://ithelp.ithome.com.tw/upload/images/20240819/201683367EofaKdxQr.png
程式碼結果探討 🧐:

  • 其實大致上跟 JsonParser 的差不多,但是 Pydantic 他可以自行設定簡單的邏輯驗證,來驗證 AI 回傳的結果是否正確。
  • 另外可以 Pydantic Parser 最終回傳是一個物件,取決於當初在 parser 給他的物件名稱,以我的例子來說,回傳的結果就是名為 Kpop 的物件。
  • 來看看 Pydantic Parser 的 format_instructions 怎麼描述的,如下圖,其實跟 Json Parser 蠻像的,都是先描述再給 few-shot
    https://ithelp.ithome.com.tw/upload/images/20240819/20168336l0FiWn9Jbl.png

Customize Parser 實戰🔥

要自定義 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?"))

程式碼結果探討 🧐:
https://ithelp.ithome.com.tw/upload/images/20240819/20168336RPy4M3MjNt.png

  • 可以看到使用 RunnableLambda 實作我的 Parser 的動作,結果就都被我轉成大寫英文。
  • 透過自定義的方式就可以拿到你想要的結果或資料結構

LCEL 結合 Runnable 實戰🔥

先說明一下我打算請 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)

https://ithelp.ithome.com.tw/upload/images/20240819/20168336dGmz1ZwWNU.png
https://ithelp.ithome.com.tw/upload/images/20240819/20168336VYXNwZ2srG.png

  1. 我省略中間的部分,先不管他程式對不對,我們可以發現 AI 的回覆內容中都歡迎語,可是我要的只有中間 html 的部分。所以我們需要自定義 Output Parser 拿出我要的部分,那可以看到在程式碼開始前後,都有三個 "`" 的符號,所以我們可以透過那個符號來進行切割。那個是 Markdown 語法中表示程式碼的語法。
  2. 切割完之後可以看到一開始那邊有一個宣稱程式是 html Markdown 格式的 "html",所以我們也需要將這個移除掉。
  3. 完成以上步驟之後,我們順利的取得 html 程式碼,所以我們透過 flask 直接將他跑起來。
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":"新增修改刪除"})

https://github.com/SeanChenR/img_gif/blob/main/2024-08-19%2021.54.40.gif?raw=true
程式碼結果探討 🧐:

  1. 首先在 Output Parser 那邊,split 取 [1] 是代表 Markdown 語法中程式碼區塊的部分,然後拿掉第一個 html ,就是乾淨的程式碼。
  2. 將 html 的原始碼取出之後,我將他寫進 index.html,然後也寫了一個 flask 的程式要讓他讀取 index.html。然後因為 flask 的 render_template 函數是要讀取同路徑中 templates 資料夾中的 html 檔,所以確認如果沒有的話要建立一個資料夾。Flask 程式的部分必須全部貼齊左邊,不然寫進 app.py 之後會大跑版。最後就是透過 os 來執行我們寫入的 app.py 檔,就可以啟動這個網頁了。
  3. 後面的部分主要都跟前面的差不多,使用 RunnableLambda 的方式讓我自定義的函數可以一起 Chain 起來,如果沒有 LangChain 應該會需要多寫好幾行程式碼!
  4. AI 真的很厲害!我都不需要修改,就可以直接寫出這樣子的網頁,超酷~

結論

今天的實戰成功!第一部分 Output Parser 的地方,如果覺得用起來很麻煩的話,不妨嘗試移除 Parser 的部分,然後進行結果的比對,應該就會理解為何他有存在的必要。第二部分將 LangChain 原生的還有自定義的都 Chain 起來了,未來希望可以越 Chain 越多啦,不僅精簡也易於擴充或修改。

題外話🤣

已經累積 15 天了~祝其他鐵人賽的參賽者大神朋友們都可以順利完成 30 天挑戰!

下一篇文章:Embedding Model 入門指南


上一篇
【Day 14】我要 Chain 好 Chain 滿!
下一篇
【Day 16】Embeddings Model 入門指南
系列文
T 大使 AI 之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言