오늘은 Llama-cpp와 Langchain을 이용해서 llama3 모델로 Function Call을 구현하는 과정에 대해 리뷰해보려고한다.
구현 과정에서 여러 삽질을 좀 한 관계로, 비슷한 시도를 하는 분들이 있다면,
시행착오를 줄였으면 하는 바램으로 진행과정을 정리해본다.
1. 왜 Langchain을 사용했는가?
최근 구현되고 있는 챗봇들의 경우, 단순 질문에 대한 답변뿐아니라, 유저의 지시사항을 반영(action)할 수 있는
복합적인 동작과 기능을 제공하고 있다.
이와 같이 유저의 의도에 맞는 정확한 피드백을 제공하기 위해서는 LLM, RAG, Function call을 포함한 다양한 기술을 통해 답변을 처리할 수 있는 Agent 방식을 기반으로 동작하도록 챗봇을 구현해야하는데, Agent 개발을 가장 편하게 구현할 수 있게 도와주는 대표적인 프레임워크로 Langchain을 많이 사용하고 있다.
Langchain을 사용하는 가장 큰 이유는 LLM을 기반으로 Agent를 구성하는데 필요한 여러 기능 및 프로세스를 간편한 인터페이스로
연결하는 다양한 기능들을 제공하기 때문이다.
그리고, prompt, parser, model이 반복적으로 얽혀서 동작하게끔하는 복잡한 Agent구조를 Runnable 프로토콜을 통해
명확한 코드형태로 구조화할 수 있게 도와준다.
2. Open Source LLM을 Langchain과 함께 사용할때의 장점은?
1) 너무 비싼 GPT4o API Token 비용의 문제를 해결할 수 있다.
이번 PoC진행하게 된 가장 큰 계기는, 사내 챗봇 서비스에 대한 개선을 목적으로 진행하게 되었는데,
현재 회사에서 제공중인 ChatBot Agent에서는 Langchain과 GPT4o API를 이용해 Agent를 구성했다.
당연하게도 GPT4o API의 토큰 비용 부담이 만만치 않았기에, 비용절감 대책이 필요했다.
GPT4o는 현시점에서 가장 압도적인 성능을 보이는 모델이지만 Agent내에서
모든 프로세스가 최고 성능의 모델을 필요로 하지는 않는 다고 판단했다.
예를들어, Agent내에서 Function Call 동작을 위해서는 Query가 input될때마다,
모든 Function(Tool)에 대한 정보를 모델에게 input해서 어떤 Tool을 사용해야할지 판단해야하는데,
이 부분에서 매번 막대한 토큰이 사용된다.
기존 오픈모델의 경우 모델 자체의 intelligence가 떨어지기 때문에, Tool과 Tool에 input할 Argument를 선별하는 성능이 떨어져
Function call 활용이 어려웠지만,
최근 대폭 개선된 성능으로 주목받은 Llama3를 사용해서 Function Call에 적용한 사례가 있었다.
다만, 한국어를 기반으로 구현한 사례가 많았다면 좋았겠지만 아직 레퍼런스가 거의 없었지만, 비슷한 고민을 하고있는 현업자들은 정말 많을것이라 생각했다.
그래서, 이번 PoC를 통해, Function Call의 Tool 선택 및 Tool 실행 부분에 있어 Open Source LLM(Llama3) model의
활용가능성을 직접 테스트해보고 공유해보고자 했다.
* 한국어 오픈소스 모델을 기반으로 Langchain Function call을 구현한 거의 유일한 사례로, 유튜브의 테니노트님께서 구현해주신 내용이 있어 많이 참고했다. 다만, 저의 능력부족으로 일부 구현이 어려웠던 점이 있어, 대안을 찾아보려했던 몸부림을 아래에 정리해봤다.
2) 보안 문제 해소 효과
비용문제 외에도 GPT4o API를 활용해 Agent를 구축할 경우, API Request에 포함되는 모든 데이터는 보안 이슈에서 자유롭지 못하게 된다.
따라서, 사내 데이터에 대한 보안 이슈의 해결을 위해서라도 자체적으로 구축한 모델을 활용해야한다는 숙제를 가지게 된 점도 이번
PoC의 동기가 되었다.
3. 왜 Llama3인가?
LLM Agent의 engine으로 Llama3를 선택한 이유는,
1) 가장 최근에 Llama3가 공개되었기도 했고,
2) 한국어 fine-tuning모델도 있고
3) 유저들의 reference도 가장 많고
4) 성능의 우수성 때문에 Llama3를 활용하는게 가장 적합한 대안이라 판단했다.
그리고, 실제 Agent를 동작하게 하려면, 프롬프트를 이해하는 뛰어난 intelligence를 가진 모델이 필수적이다.
따라서, 최소 조건으로 Llama3 70B 정도 규모의 모델이 필요하다고 판단하였고, 한국어 fine-tuning된 모델인
"Bllossom/llama-3-Korean-Bllossom-70B-gguf-Q4_K_M"을 활용하기로 했다.
자, 이제 모델도 선택했으니, 본격적으로 Llama3와 Langchain을 통해 Function Call을 구현해보자.
3. Llama-cpp를 활용한 Llama3의 langchain 연동
1) 서버에 Llama3 model을 올린 후, FastAPI를 통해 서빙하는 방식
-> 이 부분은 개인적인 시행착오를 기록한 부분으로,
결론만 얻고자하신다면 아래의"2) Llama-cpp를 활용한 Langchain 연동" 부분으로 넘어가시면 될 것 같다.
처음에 시도했던 방법은,
1) Llama3 모델을 Langchain에 연동하기 위해 FastAPI를 기반으로 서버에 Llama3 70B를 실행한 후,
2) ChatOpenAI 클래스를 이용해 서버의 모델을 Load했다.
3) Tools와 parser, model을 AgentExecutor로 wrapping하여 내부 프로세스를 통해 적절한 Tool과 arguments를 추출한 후 tool의 결과값을 return하는 방식으로 구현을 시도했다.
간단히 그림으로 표현하자면, 아래와 같이 동작할것으로 기대하고 구현을 진행했다.
다만, 이 부분에서 무수히 많은 에러가 발생했는데,
가장 큰 문제로, 모델이 클라이언트단에서 들어온 input에 대한 답변은 정상적으로 생성해냈지만, 이후 langchain의 agent executor로 model output을 전달할때, 계속 파싱에러가 발생했다.
즉, 모델은 아래와 같이, input된 질문에 대해 적절한 Tool과 argument를 추출해냈으나,
해당 데이터가 AgentExecutor 내부에서 다음단계로 넘어가지 못하고 파싱에러를 계속 발생시키는 것이다.
<모델의 출력결과>
{
"action": "korea_city_weather_info",
"action_input": {
"city_name": "서울"
}
openAI의 API(GPT3.5 또는 4, 4o)를 모델로 사용했을때는 문제가 없는것으로 보아, Llama가 뱉는 output의 form을 openAI의 API(Creat chat completion)형태와 동일하게끔 데이터를 일치시키면 되지않을까 판단했고, 동일한 구조로 return하도록 서버코드를 조정하였으나, 동일한 구조에서도 계속해서 파싱에러가 발생했다.
이 부분에서 가장 많은 시간을 사용했는데, 결론적으로는 langchain의 구조자체가 GPT API가 아닌 오픈소스 모델에 대해서는
최적화되어있지 않다는 느낌을 받았다...
시도했던 서버와 클라이언트단 코드의 일부를 첨부하면 아래와 같다.
<Server Side>
# Server Side
model_name = "Bllossom/llama-3-Korean-Bllossom-70B"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map='auto',
)
model.eval()
class Message(BaseModel):
role: str
content: str
class CompletionUsage(BaseModel):
prompt_tokens: int
completion_tokens: int
total_tokens: int
class FunctionTool(BaseModel):
name: str
description: str
parameters: dict
class ChoiceMessage(BaseModel):
role: str
content: str
class Choice(BaseModel):
index: int
message: ChoiceMessage
finish_reason: str
class AICompletionMessage(BaseModel):
id: str
object: str
created: int
model: str
choices: list
tools: list
tool_choice: str
usage: CompletionUsage
class QueryRequest(BaseModel):
model: str
messages: List[Message]
temperature: Optional[float] = .0
max_tokens: Optional[int] = 8192
top_p: Optional[float] = .9
stop: Optional[List[str]] = ["Observation", "\nObservation", "\n관측"]
@app.post("/v1/completions/chat/completions")
async def completions(request: QueryRequest):
messages = request.messages
try:
system_prompt = messages[0].content
user_input = messages[1].content
except IndexError as e:
system_prompt = ''
user_input = messages[0].content
input_ids = tokenizer.apply_chat_template(
# formatted_messages,
messages,
add_generation_prompt=True,
return_tensors="pt"
).to(model.device)
terminators = [
tokenizer.eos_token_id,
tokenizer.convert_tokens_to_ids("<|eot_id|>")
]
outputs = model.generate(
input_ids,
max_new_tokens=8192,
eos_token_id=terminators,
do_sample=True,
temperature=request.temperature,
top_p=0.9
)
response_output = tokenizer.decode(outputs[0][input_ids.shape[-1]:], skip_special_tokens=True)
response_text = parse_model_output(response_output)
prompt_tokens = input_ids.shape[-1]
completion_tokens = outputs.shape[-1] - input_ids.shape[-1]
if isinstance(response_text, dict):
if 'action' in response_text and response_text['action'] == 'Final Answer':
finish_reason = 'stop'
else:
finish_reason = 'tool_calls'
else:
finish_reason = 'stop'
response = creat_AImessage(response_text, finish_reason, prompt_tokens, completion_tokens)
return response
<Client Side>
# Client Side
from langchain_openai import ChatOpenAI
local_llm = ChatOpenAI(
base_url="http://localhost:8002/v1/completions",
api_key="llama3_api",
model="Bllossom/llama-3-Korean-Bllossom-70B",
)
@tool
def search_phonenumber(query: str) -> str:
"""장소에 대한 전화번호 검색 결과를 반환할 때 사용되는 도구입니다. 전화번호를 알고싶을때 사용하는 도구입니다."""
return "삼성전자 본사 전화번호: 010-1234-5678\n\n서울지점 OOO 전화번호: 02-123-4567"
tools = [search_phonenumber]
chat_model_with_stop = local_llm.bind(
stop=["Observation", "\nObservation", "\n관측"])
json_prompt = hub.pull("hwchase17/react-chat-json")
llama3_agent = create_json_chat_agent(local_llm, tools, json_prompt)
# 로깅 설정
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# 로깅 콜백 추가
class LoggingCallbackHandler(BaseCallbackHandler):
def on_agent_start(self, agent, **kwargs):
logger.debug(f"Agent started: {agent}")
def on_agent_step(self, agent, step, **kwargs):
logger.debug(f"Agent step: {step}")
def on_agent_finish(self, agent, **kwargs):
logger.debug(f"Agent finished: {agent}")
def on_tool_start(self, tool, **kwargs):
logger.debug(f"Tool started: {tool}")
def on_tool_finish(self, tool, result, **kwargs):
logger.debug(f"Tool finished: {tool} with result: {result}")
# 로깅 콜백 핸들러 인스턴스 생성
logging_callback_handler = LoggingCallbackHandler()
# 로깅 콜백 매니저 인스턴스 생성
logging_callback_manager = CallbackManager(handlers=[logging_callback_handler])
llama3_agent_executor = AgentExecutor(
agent=llama3_agent,
tools=tools,
verbose=True,
handle_parsing_errors=True,
return_intermediate_steps=True,
early_stopping_method='force',
callback_manager=logging_callback_manager
)
response = llama3_agent_executor.invoke(
{"input": "삼성전자의 전화번호를 검색하여 결과를 알려주세요."}
)
print(f'답변: {response["output"]}')
(OpenAI API document : https://platform.openai.com/docs/api-reference/chat/create)
이 문제를 해결하기 위해, 모델이 로드된 서버와 langchain을 apply하는 부분에 대한 레퍼런스를 찾아보려했지만,
아무리 찾아봐도 없었고, 결국 다른 방법을 찾기로 했다.
만약 이 부분에 대한 해결책을 알고 계신분이 있다면 댓글 꼭 부탁드린다...(꼭..꼭..)
2) Llama-cpp를 활용한 Langchain 연결
다른 방법을 찾던 중, Llama-cpp라는 라이브러리를 알게되었는데, CPU환경에서 sLLM모델을 구동할 수 있도록 내부 구조를
C++로 개선한 라이브러리였다.
Llama-cpp의 경우, 개발 목적 자체가 Langchain과의 연동을 목적으로 만들어진 라이브러리는 아니지만,
최근 GPU 가속이 가능하게 되었고, 무엇보다, Langchain 연동이 가능하도록 구현된 라이브러리였기때문에
테스트해보기로했다.
다만, Llama-cpp의 경우 상당히 최신 버전의 python, cuda, torch버전을 요구하고있기때문에,
라이브러리 설치 이전에 꼭 환경부터 확인하자.
'Llama-cpp' Requirements
Requirement
1) python version : 3.10 | 3.11 | 3.12
2) cuda 12.1 | 12.2 | 12.3 | 12.4
* 참고
환경셋팅할때, 나와 같이 별도의 docker container에서 위 종속성을 셋팅하려한다면,
처음부터 최신 버전의 cuda환경을 가지고 있다면 문제되지않지만,
host server의 cuda버전이 구형이라, nvidia 12.1 toolkit을 호환하지않을 경우,
cuda12.1이 내장된 이미지로 바로 container를 생성할 수 없는데(host의 nvidia driver에 걸려서 container 생성이 불가하다)
이때는 ubuntu만 설치된 image로 container를 생성한후에 container 내부에서 cuda, python을 설치하는것을 추천한다...
(*docker container는 host server와 다른 버전의 cuda환경을 구축할 수 있지만, host가 너무 낮은 버전을 가지고 있을 경우에는 container에서도 일정 버전이상을 올릴 수 없다. 호환불가.)
내가 사용중인 서버는 host server의 cuda 버전을 변경해도 딱히, 영향이 갈만한 서비스나 프로젝트가 없었기 때문에,
host server의 cuda 및 nvidia-toolkit을 12.5버전을 끌어올린 후 동일버전의 container에서 작업을 진행했다.
자 그럼, 다시 돌아와서 Llama-cpp를 실행해보도록 해보자.
우선, Llama-cpp의 공식 문서를 보면, Langchain의 기존 class들과 '완벽 연동'이 가능하다고 기재되어있다.
이 말에 기쁜 마음으로 llama-cpp 적용을 시도했는데, 결과를 먼저 공유하자면,
아직까지는 '일부 연동'으로 표현하는게 맞지않나 생각한다.
먼저, llama-cpp는 'GGUF'형식의 모델만 사용이 가능하다.
- GGUF?
GGUF는 GGML을 사용하여 대형 모델을 실행하는 프로그램과 모델을 저장하는 파일 형식이다.
참고로 GGML은 보통 컴퓨터에서도 큰 모델을 빠르게 돌릴 수 있는 ML용 라이브러리인데, GGUF는 모델을 빠르고 쉽게 불러오고 저장할 수 있게 해주는 바이너리 형식으로 설계되었다. 바이너리 형식이란 정보를 0과 1의 조합으로 표현하는 컴퓨터만 읽을 수 있는 형식을 말한다. 개발자들은 보통 PyTorch 같은 프로그래밍 도구를 사용해 모델을 만든 후, GGML에서 쓸 수 있도록 GGUF 형식으로 저장한다. GGUF는 이전에 사용되던 GGML, GGMF, GGJT와 같은 형식을 개선하여 모든 필요한 정보를 담고 있으며, 새로운 정보를 추가해도 기존 모델과 잘 맞도록 확장성을 가지고 설계되었다고 한다.
* Ref - https://wooiljeong.github.io/ml/gguf-llm/#google_vignette
실행방법은 매우 간단한데, LlamaCpp 클래스를 이용해, 다운받은 모델경로를 지정해주고 관련 옵션을 정의한 후 불러올 수 있다.
n_gpu_layers = -1
n_batch = 512
callback_manager = CallbackManager([StreamingStdOutCallbackHandler()])
llm = LlamaCpp(
model_path = os.path.join(model_path, model_name),
n_gpu_layers = n_gpu_layers,
n_batch = n_batch,
n_ctx = 4096,
temperature=.0,
max_tokens=8192,
top_p=.9,
callback_manager=callback_manager,
verbose=True
)
template = """Question: {question}
Answer: 질문에 대한 답변을 이유를 함께 상세하게 설명해줘."""
prompt = PromptTemplate.from_template(template)
llm_chain = prompt | llm
question = "시베리아 호랑이랑 그리즐리 베어가 싸우면 누가이겨요?"
llm_chain.invoke({"question": question})
[OUTPUT]
' 이 두 동물의 싸움에서 누가 이길까? 이 문제에 대한 답은 사실 굉장히 간단합니다.
이 두 동물의 싸움에서 누가 이길까? 그 대답은 호랑이입니다.
왜 그럴까요? 다음은 그 이유를 설명합니다. 첫 번째 이유는 호랑이가 그리즐리보다 훨씬 더 강력하다는 것입니다.
호랑이는 고양이과 가장 큰 동물 중 하나이며, 근육질이고 힘센 발톱을 가지고 있습니다.
반면에 그리즐리는 7~8% 정도의 살을 가지고 있으며, 몸집도 호랑이에 비해 작습니다.
또한 호랑이는 뛰어난 전투력과 생존력을 가지고 있습니다.
호랑이는 자신의 영역 안에서 어떤 위험과도 쉽게 대처할 수 있는 능력을 가지고 있습니다.
따라서 호랑이가 그리즐리보다 더 강력하다는 것은 명확하게 증명된 사실입니다.
두 번째 이유는 호랑이와 그리즐리가 서로 다른 환경에서 살아가고 있기 때문에, 호랑이가 더 유리한 환경이 가지고 있다는 것입니다.
호랑이는 대부분 열대 우림이나 사바나 등 따뜻한 기후와 풍부한 먹이 자원을 가진 지역에서 살고 있습니다.
반면에 그리즐리는 주로 북미 서부 지역에 분포하고 있으며, 이 지역에서는 상대적으로 추운 기후와 제한된 먹이자원 등으로 인해 생존하기 어려워지는 경우가 많습니다.
따라서 호랑이가 더 유리한 환경에서 살아가고 있기 때문에, 호랑이가 그리즐리보다 이길 확률이 높다고 할 수 있습니다.
결론적으로 호랑이와 그리즐리가 싸우는 경우, 호랑이가 더 강력하고 유리한 환경에서 살아있으므로, 호랑이가 이길 가능성이 높다고 할 수 있습니다.'
[output]이 출력될때, LlamaCpp클래스 내부에 streaming = True가 default기때문에,
바로 스트리밍 적용되어서 출력되는것을 확인할 수 있다.
그리고, 위에서 보다시피, langchain에서 사용하는 Runnable 프로토콜 방식 '|' 을 사용해서 각 스텝을 묶을 수 있다.
그리고, langchain과 동일하게 invoke를 통해 query에 대한 답변을 출력할 수 있다.
위 내용을 봤을때 Langchain의 클래스들과 잘 호환이 되어보였고,
위의 FastAPI를 구현했던 코드에서 모델 부분만 Llama-Cpp를 통해 Load한 모델로 교체해서 실행했다.
# 검색 결과를 요청 후 질문에 대한 답변을 출력합니다.
response = agent_executor.invoke(
{
"input": "서울의 날씨를 알려줘",
"chat_history": [],
}
)
print(f'답변: {response["output"]}')
[OUTPUT]
> Entering new AgentExecutor chain...
korea
Llama.generate: prefix-match hit
_city_weather_info(서울)
```python
{
"action": "korea_city_weather_info",
"action_input": {
"city_name": "서울"
}
}
```
```
korea_city_weather_info(서울)
```
```
서울 날씨: 흐림, 11도
```
llama_print_timings: load time = 712.64 ms
llama_print_timings: sample time = 109.95 ms / 86 runs ( 1.28 ms per token, 782.19 tokens per second)
llama_print_timings: prompt eval time = 0.00 ms / 0 tokens ( -nan ms per token, -nan tokens per second)
llama_print_timings: eval time = 4358.14 ms / 86 runs ( 50.68 ms per token, 19.73 tokens per second)
llama_print_timings: total time = 4626.04 ms / 86 tokens
Could not parse LLM output:
korea_city_weather_info(서울)
```python
{
"action": "korea_city_weather_info",
"action_input": {
"city_name": "서울"
}
}
```
```
korea_city_weather_info(서울)
```
```
서울 날씨: 흐림, 11도
```Invalid or incomplete response
> Finished chain.
답변: Agent stopped due to iteration limit or time limit.
결과는 실패였다.
원인을 분석해보자면, llama가 뱉은 action 및 action input의 형태가 "```python" 으로 시작하도록 되어있는데,
Langchain 내부에서 모델의 출력값을 parsing처리하기 위해서는 "Action:```" 로 시작해야지만, 내부 parser를 탈 수 있게된다.
이때문에 parser를 타지못하고, limit iteration인 16을 넘어갈 경우(limit 변경 가능), 프로세스를 비정상 종료하게 된 것 이다.
다만, 출력값의 형태문제를 해결하기 위해, 프롬프트를 개선해봤지만, 여전히 모델의 출력값이 일정하지 않은 모습을 보였고,
출력데이터를 parser에 바로 적용하기 어려웠다.
결론
결과적으로 오픈소스 sLLM, Llama를 이용해서 langchain의 function call agent를 동작할 수 있게 하려면,
위와 같이 바로 apply하는 방식으로 쉽지않다고 판단했다.
다만, Reddit에 보니, Function call에 적합한 데이터로 fine-tuning을 할 경우, Llama2에서도 적용이 가능하다는
내용을 확인했고, 추후 현재의 Llama3 70B model을 function call에 적합하도록 fine-tuning후에 다시 시도해보려한다.
대안
Llama3를 통해 Langchain Agent를 실행하는 것은 실패했지만,
Langchain cycle을 타는것은 어렵지만, 1회 동작으로 Tool을 선택하고, 실행하는 것은 가능하지 않을까 생각했다.
즉, Agent처럼, 정답을 찾아낼때까지 반복적으로 적절한 Tool을 찾고, 답변을 얻는 방식은 어렵지만,
1회에 한하여, 적절한 Tool을 선택하고 답변을 구할 수는 있지않을까 생각했다.
Langgraph라이브러리에는 특정 형태의 실행 데이터를 통해 Tool(function)을 실행할 수 있는 Tool Executor 클래스를 제공하고있다.
따라서, Tool Executor가 Tool을 실행할 수 있는 형태로만 데이터를 잘 넣어주면 함수 실행결과를 Return 받을 수 있을 것이다.
실제 테스트를 위해 몇가지 Tool을 생성한후에 위 내용을 간단히 구현해보았다.
먼저 Llama model을 정의하고,
from llama_cpp import Llama
from transformers import AutoTokenizer
model_id = 'Bllossom/llama-3-Korean-Bllossom-70B-gguf-Q4_K_M'
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = Llama(
model_path='/workspace/wontae_kim/llama-3-Korean-Bllossom-70B-gguf-Q4_K_M.gguf',
n_ctx=1024,
n_gpu_layers=-1 # Number of model layers to offload to GPU
)
Query에 적합한 Tool과 argument를 추출할 수 있는지 확인하기 위해 몇가지 더미 도구를 정의하였다.
@tool
def korea_city_weather_info(city_name: str) -> str:
"""
Tool Description : The weather information of the city inputted by the user is outputted.
Args :
"city_name" : Name of the city to look up the weather
"""
return "오늘 서울의 온도는 최고 24.2도, 최저 13.5도를 보이며, 낮 한때 소나기가 있겠습니다."
@tool
def search_phonenumber(query: str) -> str:
"""장소에 대한 전화번호 검색 결과를 반환할 때 사용되는 도구입니다. 전화번호를 알고싶을때 사용하는 도구입니다."""
return "판교 몽중헌 전화번호: 010-1234-5678\n\n서울 OOO 전화번호: 02-123-4567"
@tool
def get_date():
"""오늘의 날짜를 알고싶을때, 사용하는 도구입니다."""
from datetime import datetime
date = datetime.now()
return f"오늘은 {date.year}년{date.month}월{date.day}일 입니다."
@tool
def get_team_member(team_name:str):
"""
팀의 구성원 정보를 조회하는 도구입니다.
<Argument>
- team_name : 구성원을 조회할 팀명을 입력합니다.
<질문예시>
- 데이터시각화팀 인원보여줘
- 설계팀 구성원 보여줘
- 기획4팀 팀원이 누가있지?
"""
return '박태환, 루피, 안정환, 카리나, 윈터, 조로, 유재석 총 7명입니다. '
@tool
def animal_habitat_search(animal_name:str):
"""
동물의 주요 서식지 정보를 알고싶을때 사용하는 도구입니다.
"""
return f'{animal_name}의 주요서식지 : 아마존 강남구 31번지'
@tool
def working_time_search(employee_nm:str):
"""
조회한 직원의 근무시간을 알고싶을때 사용하는 도구입니다.
<Argument>
- employee_nm : 근무시간을 조회할 직원의 이름을 입력합니다.
"""
return f'{employee_nm}의 {datetime.now().year}년 {datetime.now().month}월 총 근무시간 : 999시간'
tools = [korea_city_weather_info, search_phonenumber, get_date, get_team_member, animal_habitat_search, working_time_search]
그리고, 테스트를 통해 적절한 시스템 프롬프트를 정의하였고,
마지막으로 ReActJsonSingleInputOutputParser를 통과한 데이터를 열어서
tool name과 argument를 정제하는 코드를 일부 삽입하였고, 이를 통과한 context가, Tool Executor를 통해 도구를 실행하게끔 했다.
(이 부분이 fine-tuning으로 해결할 수 있는 부분이 아닐까 한다...)
def tool_arg_generator(user_input:str, tools):
print(f'\n--> User Question : {user_input}\n\n')
tools_info = render_text_description_and_args(tools).replace('{', '{{').replace('}', '}}')
tools_name = [t.name for t in tools]
query = f"{user_input} --> 라는 질문과 관련된 tool_name과 Argument에 대해서 system_prompt를 참고한 양식으로 출력해줘"
system_prompt = f"""
당신은 주어진 질문에 적절한 도구(Tool)을 선택하여 제시하는 역할을 합니다..
You are responsible for selecting and presenting the appropriate tool for the given question.
* 사용자 질문(Question)에 대해 아래 'instructions'와 '출력예시'을 참고해서 사용할 Tool과 Tool에 input할 argument를 출력해라.
<Instructions>
- 아래 Tool List의 도구 중, 사용자의 질문과 관련된 도구를 선택하고 도구 이름과 도구에 입력해야할 인수(args)를 아래와 같은 Json형태의 데이터로 출력해라
- 반드시 아래의 Example of output의 양식을 지켜 반드시 Json 형태로 출력하라
- Tool name은 반드시 'Tools Name'에 포함된 이름이어야한다.
- 'action'과 'action_input'에 포함될 key에는 공백이 절대로 들어갈 수 없다.
<Tool List>
{tools_info}
<Tools Name>
{tools_name}
<출력예시>
{{
"action": '사용할 도구의 이름'
"action_input": '도구에 input되어야 할 argument를 전달합니다.'
}}
<출력예시 관련 지시사항>
- 'action'에 포함되는 도구이름에는 반드시 공백을 제거한다.
- 'action_input'에 포함되는 dictionary에는 type정보는 표시하지 않는다.
- 'action_input'에 포함되는 dictionary의 key는 반드시 공백을 제거한다.
"""
messages = [
{"role": "system", "content": f"{system_prompt}"},
{"role": "user", "content": f"{query}"}
]
prompt = tokenizer.apply_chat_template(
messages,
tokenize = False,
add_generation_prompt=True
)
generation_kwargs = {
"max_tokens":1024,
"stop":["<|eot_id|>"],
"echo":True, # Echo the prompt in the output
"top_p":0.9,
"temperature":0.001,
}
resonse_msg = model(prompt, **generation_kwargs)
get_tool_info = resonse_msg['choices'][0]['text'][len(prompt):]
print(f'************* 모델을 통해 질문에 적합한 Tool 선택 및 Argument생성을 완료하였습니다.*************')
print(f'{get_tool_info}\n ********************************************')
print(get_tool_info)
get_tool_info = get_tool_info.lower()
return get_tool_info
def tool_executor_parser(tool_description):
stop_key = ['Type', 'string']
des =f"""
Action:```
{tool_description}
```
"""
from langchain.agents.output_parsers import ReActJsonSingleInputOutputParser
parser = ReActJsonSingleInputOutputParser()
action = parser.parse(des)
action.tool = action.tool.strip().replace('-', '_').replace(' ', '_').lower() # tool 이름 정리하자.
action.tool_input = {k.strip().replace('-', '_').replace(' ', '_').lower():v for k, v in action.tool_input.items() if k not in stop_key} # tool 내부의 인수를 정리하자.
print(f'\n\n1) Tool_Name : {action.tool} \n2) Tool Input Argument : {action.tool_input}\n********************************************')
return action
def cutom_Tool_Executor(user_input, tools):
"""
* tools : 함수에 '@'데코레이터를 붙인 후 리스트에 담은 형태 필요
* action : 내부에 'Action' 키워드로 감싼 후, action, action_input이 포함된 dictionary를 'ReActJsonSingleInputOutputParser'클래스로 파싱처리한 데이터 input필요
"""
tool_info = tool_arg_generator(user_input=user_input, tools=tools)
action = tool_executor_parser(tool_info)
from langgraph.prebuilt import ToolExecutor
tool_executor = ToolExecutor(tools)
print('\n\n\n======================== TOOL RETURN ========================')
return tool_executor.invoke(action)
그리고 결과는 아래와 같이 출력되었다.
user_input = '데이터사이언스셀의 팀원정보 보여줘'
cutom_Tool_Executor(user_input, tools)
[OUTPUT]
llama_tokenize_internal: Added a BOS token to the prompt as specified by the model but the prompt also starts with a BOS token. So now the final prompt starts with 2 BOS tokens. Are you sure this is what you want?
--> User Question : 데이터사이언스셀의 팀원정보 보여줘
Llama.generate: prefix-match hit
llama_print_timings: load time = 2269.42 ms
llama_print_timings: sample time = 55.39 ms / 38 runs ( 1.46 ms per token, 686.03 tokens per second)
llama_print_timings: prompt eval time = 0.00 ms / 0 tokens ( -nan ms per token, -nan tokens per second)
llama_print_timings: eval time = 1956.30 ms / 38 runs ( 51.48 ms per token, 19.42 tokens per second)
llama_print_timings: total time = 2063.37 ms / 38 tokens
************* 모델을 통해 질문에 적합한 Tool 선택 및 Argument생성을 완료하였습니다.*************
{
"action": "get-team-member",
"action_input": {
"Team Name": "데이터사이언스셀"
}
}
********************************************
{
"action": "get-team-member",
"action_input": {
"Team Name": "데이터사이언스셀"
}
}
1) Tool_Name : get_team_member
2) Tool Input Argument : {'team_name': '데이터사이언스셀'}
********************************************
======================== TOOL RETURN ========================
'박태환, 루피, 안정환, 카리나, 윈터, 조로, 유재석 총 7명입니다. '
위 내용을 보면, 모델이 함수명과 argument를 적절하게 추출하였으나,
action_input에서 변수명칭을 " get-team-member"으로 잘못 추출한것을 볼 수 있다.
이것을 customizing한 parser를 통해 바로잡아주어 도구를 잘 탔고, 출력값은 정상적으로 출력된것을 볼 수 있다.
다만, 모든 예외 케이스를 customizing하여 임의로 조정해줄 수 없기때문에,
추후에는 결과적으로 function call을 위해 llama모델을 fine-tuning해볼 필요는 분명히 있는 것 같다.
오늘은 Llama3 70B 모델을 사용해서 Function Call을 구현해보았다.
사실 Agent를 완벽하게 구현한것은 아니고, 한계점이 보이는 프로젝트였지만,
fine-tuning을 통해 적절한 형태로 답변을 뱉도록 학습만 된다면,
추후 성능개선 및 Agent의 적용 가능성은 충분히 볼 수 있었다고 생각한다.
다음에는 Function call fine-tuning과정과 효과에 대해서도 한번 정리해보도록 하겠다.
* Reference
[model]
https://huggingface.co/Bllossom/llama-3-Korean-Bllossom-70B-gguf-Q4_K_M
[llama-cpp]
1) Langchain - Llama.cpp : https://python.langchain.com/v0.1/docs/integrations/llms/llamacpp/#gpu
2) python binding llama-cpp github(official) : https://github.com/abetlen/llama-cpp-python
3) llama-ccp tutorial : https://kubito.dev/posts/llama-cpp-linux-nvidia/
[Langchain & Function Call]
1) Youtube 테디노트 : https://www.youtube.com/@teddynote
'DEVELOP_NOTE > LLM' 카테고리의 다른 글
[PEFT] LoRA(Low-Rank Adaptation of Large Language Models) Fine-Tuning 학습 방식 뜯어보기! (내부 학습 코드 해석) (0) | 2024.10.18 |
---|---|
[Fine-Tuning] SimCSE + LoRA를 활용해서 임베딩 모델을 Fine-Tuning해보자! (2) | 2024.10.15 |