一 Function calling 函数调用
python
复制代码
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
from openai import OpenAI
import json
client = OpenAI()
# Example dummy function hard coded to return the same weather
# In production, this could be your backend API or an external API
def get_current_weather(location, unit="fahrenheit"):
"""Get the current weather in a given location"""
if "tokyo" in location.lower():
return json.dumps({"location": "Tokyo", "temperature": "10", "unit": unit})
elif "san francisco" in location.lower():
return json.dumps({"location": "San Francisco", "temperature": "72", "unit": unit})
elif "paris" in location.lower():
return json.dumps({"location": "Paris", "temperature": "22", "unit": unit})
else:
return json.dumps({"location": location, "temperature": "unknown"})
def run_conversation():
# Step 1: send the conversation and available functions to the model
messages = [{"role": "user", "content": "What's the weather like in San Francisco, Tokyo, and Paris?"}]
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
},
}
]
response = client.chat.completions.create(
model="gpt-3.5-turbo-0125",
messages=messages,
tools=tools,
tool_choice="auto", # auto is default, but we'll be explicit
)
print(response)
response_message = response.choices[0].message
print(response_message)
tool_calls = response_message.tool_calls
print(tool_calls)
# Step 2: check if the model wanted to call a function
if tool_calls:
# Step 3: call the function
# Note: the JSON response may not always be valid; be sure to handle errors
available_functions = {
"get_current_weather": get_current_weather,
} # only one function in this example, but you can have multiple
messages.append(response_message) # extend conversation with assistant's reply
# Step 4: send the info for each function call and function response to the model
for tool_call in tool_calls:
function_name = tool_call.function.name
function_to_call = available_functions[function_name]
function_args = json.loads(tool_call.function.arguments)
function_response = function_to_call(
location=function_args.get("location"),
unit=function_args.get("unit"),
)
messages.append(
{
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": function_response,
}
) # extend conversation with function response
second_response = client.chat.completions.create(
model="gpt-3.5-turbo-0125",
messages=messages,
) # get a new response from the model where it can see the function response
return second_response
python
复制代码
result = run_conversation()
result
result.choices[0].message.content
# 'The current weather in San Francisco is 72°F, in Tokyo it is 10°C, and in Paris it is 22°C.'
二 v3-Create-Custom-Agent
2.1 Load the LLM 加载LLM
python
复制代码
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4", temperature=0)
python
复制代码
from langchain.agents import tool
import time
@tool
def generate_unique_timestamp():
"""
生成唯一的时间戳。输入始终为空字符串。
Returns:
int: 唯一的时间戳,以毫秒为单位。
"""
timestamp = int(time.time() * 1000) # 获取当前时间的毫秒级时间戳
return timestamp
import os
@tool
def create_folder(folder_name):
"""
根据给定的文件夹名创建文件夹。
Args:
folder_name (str): 要创建的文件夹的名称。
Returns:
str: 创建的文件夹的路径。
"""
try:
os.makedirs(os.path.join("chat_history", folder_name)) # 创建文件夹
return os.path.abspath(folder_name) # 返回创建的文件夹的绝对路径
except OSError as e:
print(f"创建文件夹失败:{e}")
return None
import shutil
@tool
def delete_temp_folder():
"""
删除 chat_history 文件夹下的 temp 文件夹。输入始终为空字符串。
Returns:
bool: 如果成功删除则返回 True,否则返回 False。
"""
temp_folder = "chat_history/temp" # temp 文件夹路径
try:
shutil.rmtree(temp_folder) # 递归删除 temp 文件夹及其所有内容
print("成功删除 temp 文件夹。")
return True
except Exception as e:
print(f"删除 temp 文件夹失败:{e}")
return False
@tool
def copy_chat_history(interview_id: str) -> str:
"""
将 chat_history/temp 文件夹中的 chat_history.txt 文件复制到 chat_history 文件夹下的以 interview_id 命名的子文件夹中。
如果面试ID文件夹不存在,则返回相应的提示字符串。
参数:
interview_id (str): 面试的唯一标识符。
返回:
str: 操作结果的提示信息。
"""
# 确定临时文件夹和面试文件夹路径
temp_folder = os.path.join("chat_history", "temp")
interview_folder = os.path.join("chat_history", interview_id)
# 检查面试文件夹是否存在
if not os.path.exists(interview_folder):
return f"面试ID为 {interview_id} 的文件夹不存在。无法完成复制操作。"
# 将 chat_history.txt 从临时文件夹复制到面试文件夹
source_file = os.path.join(temp_folder, 'chat_history.txt')
destination_file = os.path.join(interview_folder, 'chat_history.txt')
shutil.copyfile(source_file, destination_file)
return f"已将 chat_history.txt 复制到面试ID为 {interview_id} 的文件夹中。"
@tool
def read_chat_history(interview_id: str) -> str:
"""
读取指定面试ID文件夹下的聊天记录(chat_history.txt)内容。
参数:
interview_id (str): 面试的唯一标识符。
返回:
str: 聊天记录的内容。
"""
# 确定面试文件夹路径
interview_folder = os.path.join("chat_history", interview_id)
# 检查面试文件夹是否存在
if not os.path.exists(interview_folder):
return f"面试ID为 {interview_id} 的文件夹不存在。无法读取聊天记录。"
# 读取聊天记录文件内容
chat_history_file = os.path.join(interview_folder, 'chat_history.txt')
with open(chat_history_file, 'r', encoding='utf-8') as file:
chat_history_content = file.read()
return chat_history_content
@tool
def generate_markdown_file(interview_id: str, interview_feedback: str) -> str:
"""
将给定的面试反馈内容生成为 Markdown 文件,并保存到指定的面试ID文件夹中。
参数:
interview_id (str): 面试的唯一标识符。
interview_feedback (str): 面试反馈的内容。
返回:
str: 操作结果的提示信息。
"""
# 确定面试文件夹路径
interview_folder = os.path.join("chat_history", interview_id)
# 检查面试文件夹是否存在
if not os.path.exists(interview_folder):
return f"面试ID为 {interview_id} 的文件夹不存在。无法生成 Markdown 文件。"
# 生成 Markdown 文件路径
markdown_file_path = os.path.join(interview_folder, "面试报告.md")
try:
# 写入 Markdown 文件
with open(markdown_file_path, 'w', encoding='utf-8') as file:
# 写入标题和面试反馈
file.write("# 面试报告\n\n")
file.write("## 面试反馈:\n\n")
file.write(interview_feedback)
file.write("\n\n")
# 读取 chat_history.txt 文件内容并写入 Markdown 文件
chat_history_file_path = os.path.join(interview_folder, "chat_history.txt")
if os.path.exists(chat_history_file_path):
file.write("## 面试记录:\n\n")
with open(chat_history_file_path, 'r', encoding='utf-8') as chat_file:
for line in chat_file:
file.write(line.rstrip('\n') + '\n\n') # 添加换行符
return f"已生成 Markdown 文件: {markdown_file_path}"
except Exception as e:
return f"生成 Markdown 文件时出错: {str(e)}"
2.3 执行
python
复制代码
from utils import parse_cv_to_md
cv_file_path = "data/cv.txt"
result = parse_cv_to_md(llm, cv_file_path)
print(result)
2.4 加载数据
python
复制代码
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from utils import parse_md_file_to_docs
file_path = "data/cv.md"
docs = parse_md_file_to_docs(file_path)
print(len(docs))
vectorstore = Chroma.from_documents(documents=docs, embedding=OpenAIEmbeddings())
retriever = vectorstore.as_retriever(search_kwargs={"k": 1})
retriever.get_relevant_documents("langchain")
python
复制代码
retriever.get_relevant_documents("python")
@tool
def find_most_relevant_block_from_cv(sentence: str) -> str:
"""
当你需要根据职位描述(JD)中的技能关键词去简历文本中找到相关内容时,就可以调用这个函数。
参数:
sentence (str): 包含技能关键词的句子。
返回:
str: 最相关的文本块。
"""
try:
most_relevant_docs = retriever.get_relevant_documents(sentence)
print(len(most_relevant_docs))
if most_relevant_docs:
most_relevant_texts = [doc.page_content for doc in most_relevant_docs]
most_relevant_text = "\n".join(most_relevant_texts)
return most_relevant_text
else:
return "未找到相关文本块"
except Exception as e:
print(f"find_most_relevant_block_from_cv()发生错误:{e}")
return "函数发生错误,未找到相关文本块"
tools = [generate_unique_timestamp, create_folder, copy_chat_history, read_chat_history, generate_markdown_file, find_most_relevant_block_from_cv]
2.5 Create Prompt 创建提示
python
复制代码
from utils import *
jd_file_path = "data/jd.txt"
jd_json_file_path = parse_jd_to_json(llm, jd_file_path)
jd_json_file_path
python
复制代码
jd_json_file_path = "data/jd.json"
jd_dict = read_json(jd_json_file_path)
jd_dict
python
复制代码
job_title = jd_dict.get('基本信息').get('职位')
job_key_skills = jd_dict.get('专业技能/知识/能力')
print(f"职位:{job_title}")
print(f"专业技能/知识/能力:{job_key_skills}")
"""职位:Python工程师 (AI应用方向)
专业技能/知识/能力:['Python', 'PyTorch', 'TensorFlow', 'Numpy', 'Redis', 'MySQL', 'openai', 'langchain', 'AI开发工具集', '向量数据库', 'Text-SQL', 'Python Web框架', 'FastAPI', 'ORM框架', 'Prompt提示词']"""
python
复制代码
# 弃用
system_prompt = f"""
## Role and Goals
- 你是所招岗位"{job_title}"的技术专家,同时也作为技术面试官向求职者提出技术问题,专注于考察应聘者的专业技能、知识和能力。
- 这里是当前岗位所需的专业技能、知识和能力:"{job_key_skills}",你应该重点围绕这些技术点提出你的问题。
- 你严格遵守面试流程进行面试。
## Interview Workflow
1. 当应聘者说开始面试后,
1.1 你要依据当前时间生成一个新的时间戳作为面试ID(只会在面试开始的时候生成面试ID,其他任何时间都不会)
1.2 以该面试ID为文件夹名创建本地文件夹(只会在面试开始的时候创建以面试ID为名的文件夹,其他任何时间都不会)
1.3 删除存储聊天记录的临时文件夹
1.4 输出该面试ID给应聘者,并结合当前技术点、与技术点相关的简历内容,提出你的第一个基础技术问题。
2. 接收应聘者的回答后,
2.1 检查应聘者的回答是否有效
2.1.1 如果是对面试官问题的正常回答(无论回答的好不好,还是回答不会,都算正常回答),就跳转到2.2处理
2.1.2 如果是与面试官问题无关的回答(胡言乱语、辱骂等),请警告求职者需要严肃对待面试,跳过2.2,再次向求职者提出上次的问题。
2.2 如果应聘者对上一个问题回答的很好,就基于当前技术点和历史记录提出一个更深入一点的问题;
如果应聘者对上一个问题回答的一般,就基于当前技术点和历史记录提出另一个角度的问题;
如果应聘者对上一个问题回答的不好,就基于当前技术点和历史记录提出一个更简单一点的问题;
如果应聘者对上一个问题表示不会、不懂、一点也回答不了,就换一个与当前技术点不同的技术点进行技术提问。
3. 当应聘者想结束面试或当应聘者想要面试报告,
3.1 从临时文件夹里复制一份聊天记录文件到当前面试ID文件夹下。
3.2 读取当前面试ID文件夹下的聊天记录,基于聊天记录、从多个角度评估应聘者的表现、生成一个详细的面试报告。
3.3 调用工具生成一个面试报告的markdown文件到当前面试ID文件夹下
3.4 告知应聘者面试已结束,以及面试报告的位置。
## Output Constraints
- 你发送给应聘者的信息中,一定不要解答你提出的面试问题,只需要有简短的反馈和提出的新问题。
- 你每次提出的技术问题,都需要结合从JD里提取的技术点和与技术点相关的简历内容,当你需要获取`与技术点相关的简历内容`时,请调用工具。
- 再一次检查你的输出,你一次只会问一个技术问题。
"""
python
复制代码
system_prompt = f"""
## Role and Goals
- 你是所招岗位"{job_title}"的技术专家,同时也作为技术面试官向求职者提出技术问题,专注于考察应聘者的专业技能、知识和能力。
- 这里是当前岗位所需的专业技能、知识和能力:"{job_key_skills}",你应该重点围绕这些技术点提出你的问题。
- 你严格遵守面试流程进行面试。
## Interview Workflow
1. 当应聘者说开始面试后,
1.1 你要依据当前时间生成一个新的时间戳作为面试ID(只会在面试开始的时候生成面试ID,其他任何时间都不会)
1.2 以该面试ID为文件夹名创建本地文件夹(只会在面试开始的时候创建以面试ID为名的文件夹,其他任何时间都不会)
1.3 删除存储聊天记录的临时文件夹
1.4 输出该面试ID给应聘者,并结合当前技术点、与技术点相关的简历内容,提出你的第一个基础技术问题。
2. 接收应聘者的回答后,
2.1 检查应聘者的回答是否有效
2.1.1 如果是对面试官问题的正常回答(无论回答的好不好,还是回答不会,都算正常回答),就跳转到2.2处理
2.1.2 如果是与面试官问题无关的回答(胡言乱语、辱骂等),请警告求职者需要严肃对待面试,跳过2.2,再次向求职者提出上次的问题。
2.2 如果应聘者对上一个问题回答的很好,就基于当前技术点和历史记录提出一个更深入一点的问题;
如果应聘者对上一个问题回答的一般,就基于当前技术点和历史记录提出另一个角度的问题;
如果应聘者对上一个问题回答的不好,就基于当前技术点和历史记录提出一个更简单一点的问题;
如果应聘者对上一个问题表示不会、不懂、一点也回答不了,就换一个与当前技术点不同的技术点进行技术提问。
3. 当应聘者想结束面试或当应聘者想要面试报告,
3.1 从临时文件夹里复制一份聊天记录文件到当前面试ID文件夹下。
3.2 读取当前面试ID文件夹下的聊天记录,基于聊天记录、从多个角度评估应聘者的表现、生成一个详细的面试报告。
3.3 调用工具生成一个面试报告的markdown文件到当前面试ID文件夹下
3.4 告知应聘者面试已结束,以及面试报告的位置。
## Output Constraints
- 你发送给应聘者的信息中,一定不要解答你提出的面试问题,只需要有简短的反馈和提出的新问题。
- 你每次提出的技术问题,都需要结合从JD里提取的技术点和与技术点相关的简历内容,当你需要获取`与技术点相关的简历内容`时,请调用工具。
- 再一次检查你的输出,你一次只会问一个技术问题。
"""
python
复制代码
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
system_prompt,
),
MessagesPlaceholder(variable_name="chat_history"),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
]
)
print(prompt.messages[0].prompt.template)
python
复制代码
llm_with_tools = llm.bind_tools(tools)
llm_with_tools.kwargs['tools']
2.7 Create the Agent 创建代理
python
复制代码
from langchain.agents.format_scratchpad.openai_tools import (
format_to_openai_tool_messages,
)
from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser
agent = (
{
"input": lambda x: x["input"],
"agent_scratchpad": lambda x: format_to_openai_tool_messages(
x["intermediate_steps"]
),
"chat_history": lambda x: x["chat_history"],
}
| prompt
| llm_with_tools
| OpenAIToolsAgentOutputParser()
)
2.8 Run the agent 运行代理
python
复制代码
from langchain.agents import AgentExecutor
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
from utils import save_chat_history
from langchain_core.messages import AIMessage, HumanMessage
chat_history = []
user_input = "开始面试"
print(user_input)
while True:
result = agent_executor.invoke({"input": user_input, "chat_history": chat_history})
print(result['output'])
chat_history.extend(
[
HumanMessage(content=user_input),
AIMessage(content=result["output"]),
]
)
# 存储聊天记录到临时文件夹
temp_folder = "chat_history/temp" # 临时文件夹名称
os.makedirs(temp_folder, exist_ok=True) # 创建临时文件夹,如果不存在则创建
save_chat_history(chat_history, temp_folder)
# 获取用户下一条输入
user_input = input("user: ")
# 检查用户输入是否为 "exit"
if user_input == "exit":
print("用户输入了 'exit',程序已退出。")
break
python
复制代码
import os
import json
from langchain_core.prompts import PromptTemplate, ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser, StrOutputParser
from langchain_core.messages import AIMessage, HumanMessage
def save_chat_history(chat_history, folder_name):
"""
将聊天记录存储到指定的文件夹下的chat_history.txt文件中。
Args:
chat_history (list): 聊天记录列表,每个元素是一个AIMessage或HumanMessage对象。
folder_name (str): 聊天记录文件夹的名称。
Returns:
str: 保存的文件路径,如果保存失败则返回None。
"""
try:
file_path = os.path.join(folder_name, "chat_history.txt") # chat_history.txt文件路径
with open(file_path, "w", encoding="utf-8") as file:
for message in chat_history:
if isinstance(message, AIMessage):
speaker = "面试官"
elif isinstance(message, HumanMessage):
speaker = "应聘者"
else:
continue # 忽略不是面试官或应聘者的消息
file.write(f"{speaker}: {message.content}\n") # 将每条聊天记录写入文件,每条记录占一行
return file_path # 返回保存的文件路径
except Exception as e:
print(f"保存聊天记录失败:{e}")
return None
def parse_jd_to_json(llm, jd_file_path: str):
"""
将给定的 JD 文件内容解析为 JSON,并存储到指定路径下。
参数:
llm: 大模型。
jd_file_path (str): JD 文件的路径。
返回:
str: 存储的 JSON 文件路径。
"""
try:
with open(jd_file_path, 'r', encoding='utf-8') as jd_file:
jd_content = jd_file.read().strip()
template = """
基于JD文本,按照约束,生成以下格式的 JSON 数据:
{{
"基本信息": {{
"职位": "职位名称",
"薪资": "薪资范围",
"地点": "工作地点",
"经验要求": "经验要求",
"学历要求": "学历要求",
"其他":""
}},
"岗位职责": {{
"具体职责": ["职责1", "职责2", ...]
}},
"岗位要求": {{
"学历背景": "学历要求",
"工作经验": "工作经验要求",
"技能要求": ["技能1", "技能2", ...],
"个人特质": ["特质1", "特质2", ...],
}},
"专业技能/知识/能力": ["技能1", "技能2", ...],
"其他信息": {{}}
}}
JD文本:
[{jd_content}]
约束:
1、除了`专业技能/知识/能力`键,其他键的值都从原文中获取。
2、保证JSON里的值全面覆盖JD原文,不遗漏任何原文,不知如何分类就放到`其他信息`里。
3、`专业技能/知识/能力`键对应的值要求从JD全文中(尤其是岗位职责、技能要求部分)提取总结关键词或关键短句,不能有任何遗漏的硬技能。
JSON:
"""
parser = JsonOutputParser()
prompt = PromptTemplate(
template=template,
input_variables=["jd_content"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
print(prompt.template)
chain = prompt | llm | parser
result = chain.invoke({"jd_content": jd_content})
# 打印
print(result)
print(type(result))
print(result['专业技能/知识/能力'])
# 存储到 data 目录下
output_file_path = "data/jd.json"
with open(output_file_path, 'w', encoding='utf-8') as output_file:
json.dump(result, output_file, ensure_ascii=False)
print(f"已存储最终 JSON 文件到 {output_file_path}")
return output_file_path
except Exception as e:
print(f"解析 JD 文件时出错: {str(e)}")
return None
def read_json(file_path: str) -> dict:
"""
读取 JSON 文件并返回其内容。
参数:
file_path (str): JSON 文件的路径。
返回:
dict: JSON 文件的内容。
"""
try:
with open(file_path, 'r', encoding='utf-8') as json_file:
data = json.load(json_file)
return data
except Exception as e:
print(f"读取 JSON 文件时出错: {str(e)}")
return {}
from langchain.text_splitter import MarkdownHeaderTextSplitter
from langchain.schema import Document
def parse_cv_to_md(llm, cv_file_path: str):
"""
将给定的简历文件内容解析为 JSON,并存储到指定路径下。
参数:
llm: 大模型。
cv_file_path (str): 简历文件的路径。
返回:
str: 存储的 Markdown 文件路径。
"""
try:
with open(cv_file_path, 'r', encoding='utf-8') as cv_file:
cv_content = cv_file.read().strip()
template = """
基于简历文本,按照约束,转换成Markdown格式:
简历文本:
[{cv_content}]
约束:
1、只用一级标题和二级标题分出来简历的大块和小块
2、一级标题只有这些:个人信息、教育经历、工作经历、项目经历、校园经历、职业技能、曾获奖项、兴趣爱好、自我评价、其他信息。
Markdown:
"""
parser = StrOutputParser()
prompt = PromptTemplate(
template=template,
input_variables=["cv_content"]
)
print(prompt.template)
chain = prompt | llm | parser
result = chain.invoke({"cv_content": cv_content})
# 打印
print(result)
print(type(result))
# 存储到 data 目录下
output_file_path = "data/cv.md"
with open(output_file_path, 'w', encoding='utf-8') as output_file:
output_file.write(result.strip("```"))
output_file.write("\n\n")
print(f"已存储最终 Markdown 文件到 {output_file_path}")
return output_file_path
except Exception as e:
print(f"解析 CV 文件时出错: {str(e)}")
return None
def parse_md_file_to_docs(file_path):
with open(file_path, 'r', encoding='utf-8') as file:
markdown_text = file.read()
docs = []
headers_to_split_on = [
("#", "Title 1"),
("##", "Title 2"),
# ("###", "Title 3"),
# ("###", "Title 4")
]
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
split_docs = markdown_splitter.split_text(markdown_text)
for split_doc in split_docs:
metadata = split_doc.metadata
title_str = f"# {metadata.get('Title 1', 'None')}\n## {metadata.get('Title 2', 'None')}\n"
page_content = title_str + split_doc.page_content.strip()
doc = Document(
page_content=page_content,
metadata=metadata
)
docs.append(doc)
return docs
if __name__ == "__main__":
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4", temperature=0) # gpt-3.5-turbo
# # jd
# jd_file_path = "data/jd.txt"
# result = parse_jd_to_json(llm, jd_file_path)
# print(result)
# cv
cv_file_path = "data/cv.txt"
result = parse_cv_to_md(llm, cv_file_path)
print(result)