Skip to content

Deveplop/update character glm #103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions lesson/00_简介.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
此项目利用CogView和CharGLM,开发一个能够进行图像生成和文字聊天的情感陪聊助手,探讨其在心理健康和社交互动中的潜力。

项目分为4个部分
1. 类型标注介绍与数据类型定义
2. CogView和CharacterGLM API
3. 开发图像生成和角色扮演的聊天机器人

运行环境:python>=3.8
依赖库:
* pyjwt
* requests
* streamlit
* zhipuai
* python-dotenv
29 changes: 29 additions & 0 deletions lesson/01_类型标注介绍与数据类型定义.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Python运行时并不强制标注函数和变量类型。类型标注可被用于第三方工具,比如类型检查器、集成开发环境、静态检查器等。
Python的`typing`模块和`typing_extensions`模块为类型标注提供了支持,阅读文档<https://docs.python.org/zh-cn/3.8/library/typing.html>可了解`typing`模块的用法。

本项目首先实现`data_types.py`,完成相关数据类型的定义。`data_types.py`的功能比较简单,无需赘述。下面主要介绍`data_types.py`采用的部分类型标注技巧。
1. [`TypedDict`](https://docs.python.org/zh-cn/3.8/library/typing.html#typing.TypedDict)

`TypedDict`能定义带类型标注的字典类型。在编写代码时,IDE能提示该字典包含的字段及其类型。下图的示例中,VSCode提示了TextMsg包含`role`字段。由于`role`字段是Literal["user", "assistant"]类型,VSCode展示了它的2个可选值。

![](./images/01_代码提示.png)
在运行时,类型信息会被忽略,`TypedDict`创建的字典和普通字典完全相同,尝试执行

```bash
python data_types.py
```

可以观察到输出结果

```Plain Text
<class 'dict'>
{'role': 'user', 'content': '42'}
```
2. [TYPE_CHECKING](https://docs.python.org/zh-cn/3.8/library/typing.html#typing.TYPE_CHECKING) & [ForwardRef](https://docs.python.org/zh-cn/3.8/library/typing.html#typing.ForwardRef)

`typing.TYPE_CHECKING`是被第三方静态类型检查器假定为 True 的特殊常量。但在运行时的值为False。
`ImageMsg``image`字段展示`ForwardRef`的用法。IDE能正确地提示`image`字段的类型,但在运行时,由于`TYPE_CHECKING`为False,`streamlit.elements.image`不会被导入,避免`data_types`依赖`streamlit`模块。



作业1:除了`typing.TypedDict`之外,也有其他的库用类型标注的方式完成数据类型定义。请了解`dataclasses``pydantic`的用法,尝试用`dataclasses.dataclass``pydantic.BaseModel`实现`TextMsg`。思考这三种方式各自有何优缺点。
46 changes: 46 additions & 0 deletions lesson/02_CogView和CharacterGLM API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
`api.py`实现了调用CogView和CharacterGLM的代码,包括
1. `generate_cogview_image`:用`zhipuai`库(官方sdk)调用CogView,返回图片url,可通过浏览器打开url查看图片,也可以用requests下载图片。参考<https://open.bigmodel.cn/dev/api#cogview>
2. `get_characterglm_response`:用`requests`库调用CharacterGLM,获得流式响应,参考<https://open.bigmodel.cn/dev/api#characterglm>

这两个API都需要智谱开放平台API key,参考 <https://open.bigmodel.cn/usercenter/apikeys>


`cogview_example.py`展示了一个CogView的示例,执行该脚本,输出如下:
```Plain Text
image_prompt:
国画,孤舟蓑笠翁,独钓寒江雪
image_url:
https://sfile.chatglm.cn/testpath/af9a2333-1b8e-58e7-9d9f-9ac52934935c_0.png
```

浏览器打开url,可以查看生成的图片。注意:每次执行该脚本,都会生成新的图片,您的生成结果可能与示例结果不同。

![](./images/02_cogview_result.png)


`characterglm_example.py`展示了一个CharacterGLM的示例,执行该脚本,输出如下:
```Plain Text
眼神
变得
真的
可是
觉得自己
不了
多久
```
注意:每次执行该脚本,都会生成新的图片,您的生成结果可能与示例结果不同。


作业2-1:为了提高并发数,许多python程序会采用异步方式(async/await)调用API,请尝试实现异步的CharacterGLM API。提示:可以用aiohttp或httpx等异步请求库代替`get_characterglm_response`采用的requests库。

作业2-2:尝试修改文生图的prompt,生成不同风格的图片,例如,油画、水墨画、动漫等风格。
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[streamlit](https://streamlit.io/)是一个开源Python库,可以轻松创建和共享用于机器学习和数据科学的漂亮的自定义web应用程序。即使开发者不擅长前端开发,也能快速的构建一个比较漂亮的页面。

`characterglm_api_demo_streamlit.py`展示了一个具备图像生成和角色扮演能力的聊天机器人。它用`streamlit`构建了界面,调用CogView API实现文生图,调用CharacterGLM API实现角色扮演。执行下列命令可启动demo,其中`--server.address 127.0.0.1`是可选参数。
```bash
streamlit run --server.address 127.0.0.1 characterglm_api_demo_streamlit.py
```

作业3:改进代码,为文生图功能加上风格选项,在页面上加上一个可指定图片风格的选项框。
Binary file added lesson/__pycache__/api.cpython-311.pyc
Binary file not shown.
Binary file added lesson/__pycache__/api.cpython-39.pyc
Binary file not shown.
Binary file added lesson/__pycache__/data_types.cpython-311.pyc
Binary file not shown.
Binary file added lesson/__pycache__/data_types.cpython-39.pyc
Binary file not shown.
181 changes: 181 additions & 0 deletions lesson/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import requests
import time
import os
from typing import Generator

import jwt

from data_types import TextMsg, ImageMsg, TextMsgList, MsgList, CharacterMeta


# 智谱开放平台API key,参考 https://open.bigmodel.cn/usercenter/apikeys
API_KEY: str = os.getenv("ZHIPUAI_API_KEY")


class ApiKeyNotSet(ValueError):
pass


def verify_api_key_not_empty():
if not API_KEY:
raise ApiKeyNotSet


def generate_token(apikey: str, exp_seconds: int) -> str:
# reference: https://open.bigmodel.cn/dev/api#nosdk
try:
id, secret = apikey.split(".")
except Exception as e:
raise Exception("invalid apikey", e)

payload = {
"api_key": id,
"exp": int(round(time.time() * 1000)) + exp_seconds * 1000,
"timestamp": int(round(time.time() * 1000)),
}

return jwt.encode(
payload,
secret,
algorithm="HS256",
headers={"alg": "HS256", "sign_type": "SIGN"},
)


def get_characterglm_response(messages: TextMsgList, meta: CharacterMeta) -> Generator[str, None, None]:
""" 通过http调用characterglm """
# Reference: https://open.bigmodel.cn/dev/api#characterglm
verify_api_key_not_empty()
url = "https://open.bigmodel.cn/api/paas/v3/model-api/charglm-3/sse-invoke"
resp = requests.post(
url,
headers={"Authorization": generate_token(API_KEY, 1800)},
json=dict(
model="charglm-3",
meta=meta,
prompt=messages,
incremental=True)
)
resp.raise_for_status()

# 解析响应(非官方实现)
sep = b':'
last_event = None
for line in resp.iter_lines():
if not line or line.startswith(sep):
continue
field, value = line.split(sep, maxsplit=1)
if field == b'event':
last_event = value
elif field == b'data' and last_event == b'add':
yield value.decode()


def get_characterglm_response_via_sdk(messages: TextMsgList, meta: CharacterMeta) -> Generator[str, None, None]:
""" 通过旧版sdk调用characterglm """
# 与get_characterglm_response等价
# Reference: https://open.bigmodel.cn/dev/api#characterglm
# 需要安装旧版sdk,zhipuai==1.0.7
import zhipuai
verify_api_key_not_empty()
zhipuai.api_key = API_KEY
response = zhipuai.model_api.sse_invoke(
model="charglm-3",
meta= meta,
prompt= messages,
incremental=True
)
for event in response.events():
if event.event == 'add':
yield event.data


def get_chatglm_response_via_sdk(messages: TextMsgList) -> Generator[str, None, None]:
""" 通过sdk调用chatglm """
# reference: https://open.bigmodel.cn/dev/api#glm-3-turbo `GLM-3-Turbo`相关内容
# 需要安装新版zhipuai
from zhipuai import ZhipuAI
verify_api_key_not_empty()
client = ZhipuAI(api_key=API_KEY) # 请填写您自己的APIKey
response = client.chat.completions.create(
model="glm-3-turbo", # 填写需要调用的模型名称
messages=messages,
stream=True,
)
for chunk in response:
yield chunk.choices[0].delta.content


def generate_role_appearance(role_profile: str) -> Generator[str, None, None]:
""" 用chatglm生成角色的外貌描写 """

instruction = f"""
请从下列文本中,抽取人物的外貌描写。若文本中不包含外貌描写,请你推测人物的性别、年龄,并生成一段外貌描写。要求:
1. 只生成外貌描写,不要生成任何多余的内容。
2. 外貌描写不能包含敏感词,人物形象需得体。
3. 尽量用短语描写,而不是完整的句子。
4. 不要超过50字
文本:
{role_profile}
"""
return get_chatglm_response_via_sdk(
messages=[
{
"role": "user",
"content": instruction.strip()
}
]
)


def generate_chat_scene_prompt(messages: TextMsgList, meta: CharacterMeta) -> Generator[str, None, None]:
""" 调用chatglm生成cogview的prompt,描写对话场景 """
instruction = f"""
阅读下面的角色人设与对话,生成一段文字描写场景。
{meta['bot_name']}的人设:
{meta['bot_info']}
""".strip()

if meta["user_info"]:
instruction += f"""
{meta["user_name"]}的人设:
{meta["user_info"]}
""".rstrip()

if messages:
instruction += "\n\n对话:" + '\n'.join((meta['bot_name'] if msg['role'] == "assistant" else meta['user_name']) + ':' + msg['content'].strip() for msg in messages)

instruction += """
要求如下:
1. 只生成场景描写,不要生成任何多余的内容
2. 描写不能包含敏感词,人物形象需得体
3. 尽量用短语描写,而不是完整的句子
4. 不要超过50字
""".rstrip()
print(instruction)

return get_chatglm_response_via_sdk(
messages=[
{
"role": "user",
"content": instruction.strip()
}
]
)


def generate_cogview_image(prompt: str) -> str:
""" 调用cogview生成图片,返回url """
# reference: https://open.bigmodel.cn/dev/api#cogview
from zhipuai import ZhipuAI
client = ZhipuAI() # 请填写您自己的APIKey

response = client.images.generations(
model="cogview-3", #填写需要调用的模型名称
prompt=prompt
)
return response.data[0].url
266 changes: 266 additions & 0 deletions lesson/characterglm_api_demo_streamlit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
"""
一个简单的demo,调用CharacterGLM实现角色扮演,调用CogView生成图片,调用ChatGLM生成CogView所需的prompt。
依赖:
pyjwt
requests
streamlit
zhipuai
python-dotenv
运行方式:
```bash
streamlit run characterglm_api_demo_streamlit.py
```
"""
import os
import itertools
from typing import Iterator, Optional

import streamlit as st
from dotenv import load_dotenv
# 通过.env文件设置环境变量
# reference: https://github.com/theskumar/python-dotenv
load_dotenv()

import api
from api import generate_chat_scene_prompt, generate_role_appearance, get_characterglm_response, generate_cogview_image
from data_types import TextMsg, ImageMsg, TextMsgList, MsgList, filter_text_msg

st.set_page_config(page_title="CharacterGLM API Demo", page_icon="🤖", layout="wide")
debug = os.getenv("DEBUG", "").lower() in ("1", "yes", "y", "true", "t", "on")


def update_api_key(key: Optional[str] = None):
if debug:
print(f'update_api_key. st.session_state["API_KEY"] = {st.session_state["API_KEY"]}, key = {key}')
key = key or st.session_state["API_KEY"]
if key:
api.API_KEY = key

# 设置API KEY
api_key = st.sidebar.text_input("API_KEY", value=os.getenv("ZHIPUAI_API_KEY"), key="API_KEY", type="password", on_change=update_api_key)
update_api_key(api_key)


# 初始化
if "history" not in st.session_state:
st.session_state["history"] = []
if "meta" not in st.session_state:
st.session_state["meta"] = {
"user_info": "",
"bot_info": "",
"bot_name": "",
"user_name": ""
}


def init_session():
st.session_state["history"] = []


# 4个输入框,设置meta的4个字段
meta_labels = {
"bot_name": "角色名",
"user_name": "用户名",
"bot_info": "角色人设",
"user_info": "用户人设"
}

# 2x2 layout
with st.container():
col1, col2 = st.columns(2)
with col1:
st.text_input(label="角色名", key="bot_name", on_change=lambda : st.session_state["meta"].update(bot_name=st.session_state["bot_name"]), help="模型所扮演的角色的名字,不可以为空")
st.text_area(label="角色人设", key="bot_info", on_change=lambda : st.session_state["meta"].update(bot_info=st.session_state["bot_info"]), help="角色的详细人设信息,不可以为空")

with col2:
st.text_input(label="用户名", value="用户", key="user_name", on_change=lambda : st.session_state["meta"].update(user_name=st.session_state["user_name"]), help="用户的名字,默认为用户")
st.text_area(label="用户人设", value="", key="user_info", on_change=lambda : st.session_state["meta"].update(user_info=st.session_state["user_info"]), help="用户的详细人设信息,可以为空")


def verify_meta() -> bool:
# 检查`角色名`和`角色人设`是否空,若为空,则弹出提醒
if st.session_state["meta"]["bot_name"] == "" or st.session_state["meta"]["bot_info"] == "":
st.error("角色名和角色人设不能为空")
return False
else:
return True


def draw_new_image(style_description = '二次元风格。'):
"""生成一张图片,并展示在页面上"""
if not verify_meta():
return
text_messages = filter_text_msg(st.session_state["history"])
if text_messages:
# 若有对话历史,则结合角色人设和对话历史生成图片
image_prompt = "".join(
generate_chat_scene_prompt(
text_messages[-10: ],
meta=st.session_state["meta"]
)
)
else:
# 若没有对话历史,则根据角色人设生成图片
image_prompt = "".join(generate_role_appearance(st.session_state["meta"]["bot_info"]))

if not image_prompt:
st.error("调用chatglm生成Cogview prompt出错")
return

# TODO: 加上风格选项
image_prompt = style_description + image_prompt.strip()

print(f"image_prompt = {image_prompt}")
n_retry = 3
st.markdown("正在生成图片,请稍等...")
for i in range(n_retry):
try:
img_url = generate_cogview_image(image_prompt)
except Exception as e:
if i < n_retry - 1:
st.error("遇到了一点小问题,重试中...")
else:
st.error("又失败啦,点击【生成图片】按钮可再次重试")
return
else:
break
img_msg = ImageMsg({"role": "image", "image": img_url, "caption": image_prompt})
# 若history的末尾有图片消息,则替换它,(重新生成)
# 否则,append(新增)
while st.session_state["history"] and st.session_state["history"][-1]["role"] == "image":
st.session_state["history"].pop()
st.session_state["history"].append(img_msg)
st.rerun()


button_labels = {
"clear_meta": "清空人设",
"clear_history": "清空对话历史",
"picture_syle":"图片风格",
"gen_picture": "生成图片",
"show_history": "显示对话数据",
"download_file":"保存对话数据"
}
if debug:
button_labels.update({
"show_api_key": "查看API_KEY",
"show_meta": "查看meta",

})

# 在同一行排列按钮
with st.container():
n_button = len(button_labels)
cols = st.columns(n_button)
button_key_to_col = dict(zip(button_labels.keys(), cols))

with button_key_to_col["clear_meta"]:
clear_meta = st.button(button_labels["clear_meta"], key="clear_meta")
if clear_meta:
st.session_state["meta"] = {
"user_info": "",
"bot_info": "",
"bot_name": "",
"user_name": ""
}
st.rerun()

with button_key_to_col["clear_history"]:
clear_history = st.button(button_labels["clear_history"], key="clear_history")
if clear_history:
init_session()
st.rerun()

with button_key_to_col["gen_picture"]:
gen_picture = st.button(button_labels["gen_picture"], key="gen_picture")
style_description = st.text_input("输入图像风格描述")

with button_key_to_col["show_history"]:
show_history = st.button(button_labels["show_history"], key="show_history")
if show_history:
print(f"history = {st.session_state['history']}")

with button_key_to_col["download_file"]:
download_file = st.button(button_labels["download_file"], key="download_file")
if download_file:
history = st.session_state['history']
formatted_history = "\n\n".join([f"{entry['role']}:{entry['content']}" for entry in history])
st.download_button("确认保存对话", formatted_history,'Content.md')


if debug:
with button_key_to_col["show_api_key"]:
show_api_key = st.button(button_labels["show_api_key"], key="show_api_key")
if show_api_key:
print(f"API_KEY = {api.API_KEY}")

with button_key_to_col["show_meta"]:
show_meta = st.button(button_labels["show_meta"], key="show_meta")
if show_meta:
print(f"meta = {st.session_state['meta']}")

if gen_picture:
draw_new_image(style_description)


# 展示对话历史
for msg in st.session_state["history"]:
if msg["role"] == "user":
with st.chat_message(name="user", avatar="user"):
st.markdown(msg["content"])
elif msg["role"] == "assistant":
with st.chat_message(name="assistant", avatar="assistant"):
st.markdown(msg["content"])
elif msg["role"] == "image":
with st.chat_message(name="assistant", avatar="assistant"):
st.image(msg["image"], caption=msg.get("caption", None))
else:
raise Exception("Invalid role")



with st.chat_message(name="user", avatar="💥"):
input_placeholder = st.empty()
with st.chat_message(name="assistant", avatar="assistant"):
message_placeholder = st.empty()


def output_stream_response(response_stream: Iterator[str], placeholder):
content = ""
for content in itertools.accumulate(response_stream):
placeholder.markdown(content)
return content


def start_chat():
query = st.chat_input("初始化对话:输入用户角色第一条消息")
if not query:
return
else:
if not verify_meta():
return
if not api.API_KEY:
st.error("未设置API_KEY")

input_placeholder.markdown(query)
st.session_state["history"].append(TextMsg({"role": "user", "content": query}))
while len(st.session_state["history"]) <= 10:
response_stream = get_characterglm_response(filter_text_msg(st.session_state["history"]), meta=st.session_state["meta"])
bot_response = output_stream_response(response_stream, message_placeholder)
if not bot_response:
message_placeholder.markdown("生成出错")
st.session_state["history"].pop()
else:
st.session_state["history"].append(TextMsg({"role": "assistant", "content": bot_response}))
query_stream = get_characterglm_response(filter_text_msg(st.session_state["history"]), meta=st.session_state["meta"])
query_response = output_stream_response(query_stream, input_placeholder)
if not query_response:
message_placeholder.markdown("生成出错")
st.session_state["history"].pop()
else:
st.session_state["history"].append(TextMsg({"role": "user", "content": query_response}))


start_chat()
25 changes: 25 additions & 0 deletions lesson/characterglm_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import time
from dotenv import load_dotenv
load_dotenv()

from api import get_characterglm_response


def characterglm_example():
character_meta = {
"user_info": "",
"bot_info": "小白,性别女,17岁,平溪孤儿院的孩子。小白患有先天的白血病,头发为银白色。小白身高158cm,体重43kg。小白的名字是孤儿院院长给起的名字,因为小白是在漫天大雪白茫茫的一片土地上被捡到的。小白经常穿一身破旧的红裙子,只是为了让自己的气色看上去红润一些。小白初中毕业,没有上高中,学历水平比较低。小白在孤儿院相处最好的一个人是阿南,小白喊阿南哥哥。阿南对小白很好。",
"user_name": "用户",
"bot_name": "小白"
}
messages = [
{"role": "assistant", "content": "哥哥,我会死吗?"},
{"role": "user", "content": "(微信)怎么会呢?医生说你的病情已经好转了"}
]
for chunk in get_characterglm_response(messages, meta=character_meta):
print(chunk)
time.sleep(0.5)


if __name__ == "__main__":
characterglm_example()
18 changes: 18 additions & 0 deletions lesson/cogview_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from dotenv import load_dotenv
load_dotenv()

from api import generate_cogview_image


def cogview_example():
image_prompt = "国画,孤舟蓑笠翁,独钓寒江雪"
image_url = generate_cogview_image(image_prompt)

print("image_prompt:")
print(image_prompt)
print("image_url:")
print(image_url)


if __name__ == "__main__":
cogview_example()
57 changes: 57 additions & 0 deletions lesson/data_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
相关数据类型的定义
"""
from typing import Literal, TypedDict, List, Union, Optional, TYPE_CHECKING
if TYPE_CHECKING:
import streamlit.elements.image

class BaseMsg(TypedDict):
pass


class TextMsg(BaseMsg):
"""文本消息"""

# 在类属性标注的下一行用三引号注释,vscode中
role: Literal["user", "assistant"]
"""消息来源"""
content: str
"""消息内容"""


class ImageMsg(BaseMsg):
"""图片消息"""
role: Literal["image"]
image: "streamlit.elements.image.ImageOrImageList"
"""图片内容"""
caption: Optional[Union[str, List[str]]]
"""说明文字"""


Msg = Union[TextMsg, ImageMsg]
TextMsgList = List[TextMsg]
MsgList = List[Msg]


class CharacterMeta(TypedDict):
"""角色扮演设定,它是CharacterGLM API所需的参数"""
user_info: str
"""用户人设"""
bot_info: str
"""角色人设"""
bot_name: str
"""bot扮演的角色的名字"""
user_name: str
"""用户的名字"""


def filter_text_msg(messages: MsgList) -> TextMsgList:
return [m for m in messages if m["role"] != "image"]


if __name__ == "__main__":
# 尝试在VSCode等IDE中自己敲一遍下面的代码,观察IDE能提供哪些代码提示
text_msg = TextMsg(role="user")
text_msg["content"] = "42"
print(type(text_msg))
print(text_msg)
Binary file added lesson/images/01_代码提示.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added lesson/images/02_cogview_result.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added lesson/images/image-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added lesson/images/image-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions lesson/tests/Content (1).md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
user:哇,你在看书吗(好奇,搭讪男生)

assistant:是啊,你怎么会看武侠小说呢(笑)

user:你看起来不像喜欢这种书的人(开玩笑)

assistant:(笑)怎么说话的,我可是武侠迷呢。

user:你也喜欢吗?

assistant:是啊,我特别喜欢金庸和古龙。

user:你是第一次看吗?

assistant:是啊,你呢?

user:我啊,我看了很多次了,每一部都看过(自豪)

assistant:你都看过吗?好厉害啊。

user:(惊讶)你这么喜欢吗?
21 changes: 21 additions & 0 deletions lesson/tests/Content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
user:哇,你在看书吗(好奇,搭讪男生)

assistant:是啊,这本书很有趣(微笑回应)

user:你也喜欢看书吗?

assistant:当然,我是一个书虫。

user:你喜欢什么类型的书?

assistant:我什么书都喜欢,不过最喜欢武侠小说。

user:(惊讶)你也喜欢武侠小说吗?我最喜欢桃花岛主黄药师了!

assistant:(惊喜)你也喜欢黄药师?我也超级喜欢他!

user:他的武功高强,而且特立独行,不拘泥于世俗的规矩。

assistant:没错,他是一个真正的侠客,我也很崇拜他。

user:你知道吗,我一直梦想着成为像黄药师一样的侠客,浪迹天涯,行侠仗义。
4 changes: 4 additions & 0 deletions lesson/运行截图.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
![image-2](images/image-1.png)

![image-2](images/image-2.png)