PS:之前系列名字『AI 开发实战』总感觉像是买课的,还是 9.9 那种,所以之后系列合集名字更新成:《万物皆可 LLM》。
上回说到怎么自定义 LangChain 的工具,今天正好做 PPT,图片配色成了个问题。 秉承 万事万物皆可 LLM 的原则,颜色啥的必须让 LLM 搞定。
先看结果:
可以实现指定颜色生成色板,比如提问:
生成 #8A2BE2 的色板
也可以支持描述颜色生成,比如提问:
生成淡蓝色色板
# or
中国传统颜色水红色色板
# or
活泼可爱一点的颜色色板
当然描述颜色生成色板可能一下子达不到预期,比如:
可以先让 GPT 生成一些颜色,再挑选喜欢的颜色生成,
然后指定 RGB 生成:
UI 改动
UI 也有两个改动 :
• 增加了工具栏,可以点击切换当前使用那些工具
• 消息实现了颜色解析,虽然还有点儿小 Bug,无伤大雅
接下来看怎么实现。
后端
后端的流程从入口开始讲,好理解一点。
定义接口
先约定 API 结构,我们使用 POST 格式传递数据,定义三个参数:
• llm:使用哪个大语言模型
• prompt:对话提示语
• tools:启用的工具
这三个参数都是前端传递过来的,代码如下:
class Prompt(BaseModel):
llm: str
prompt: str
tools: list = []
处理工具
工具的话我们使用 LangChain 的 agent 加载,由于接口参数不允许0工具调用,我们简单粗暴的判断一下,如果客户端选择了工具就是用 agent, 如果没有选择工具就直接对话:
@app.post("/chat/")
async def create_item(item: Prompt):
llm = llms[item.llm]
logger.info('Prompt: ' + item.prompt)
tool = [tools.get(x) for x in item.tools if tools.__contains__(x)]
if tool:
agent = initialize_agent(tool, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
answer = agent.run(item.prompt)
logger.info(answer)
return {
'content': answer,
'llm': item.llm
}
else:
messages = [
SystemMessage(content="简单回答,使用中文"),
HumanMessage(content=item.prompt)
]
answer = llm(messages).content
logger.info(answer)
return {
'content': answer,
'llm': item.llm
}
llms 实现
我们把 llm 重构到单独模块,让代码清晰一点:
claude = ChatClaude()
openai = ChatOpenAI()
bard = ChatBard()
llms = {
"claude": claude,
"openai": openai,
"bard": bard
}
tools 实现
同样,tools 也重构到单独模块,使用的地方可以根据 key 选择,不用关心实现:
tools = {
'colors': ColorTool(),
'random-color': RandomColorTool(),
'balabala': Balabala()
}
colors tool 实现
实现色板的代码并无新意,根据 HSL 明度和保护度递增递减。
我这版本实现的比较粗糙,想让变化曲线更平滑一点,但是值去的并不好,后面有时间用其他曲线来替换正弦曲线。
RGB 颜色 和 HSL 颜色的转换可以用下面函数实现:
colorsys.rgb_to_hls
colorsys.hls_to_rgb
生成五个浅色,默认输入颜色为第六个颜色(主色),如果输入颜色饱和明暗不够,最终的色板可能会有多个黑色或者白色。
浅色生成:
# 浅色
count = 5
for i in range(count):
h -= hue_step
l -= math.sin(light_step) * (i + 1) / (count + 3)
s += math.sin(sat_step) * i / 20
if l < 0:
print('l=0')
l = 0
if s > 1:
print('s=1')
s = 1
colors.insert(0, colorsys.hls_to_rgb(h, l, s))
同样再生成四个深色:
for i in range(count):
h += hue_step
l += math.sin(light_step) * (i + 1) / (count)
s -= math.sin(sat_step) * i / 20
if l > 1:
print('l=1')
l = 1
if s < 0:
print('s=0')
s = 0
color_scale.append(colorsys.hls_to_rgb(h, l, s))
LangChain Tool 实现
实现 Tool 和上文讲的一样,我这里提示写了好几个版本,效果有些差别但是差别不大。
中英文提示也有差异,但是 LLM 一般都是翻译成英文执行的,所以测试下来中文写还是英文写 LLM 自动翻译成英文之后也大差不差。
class ColorsTool(BaseTool):
name = "colors"
# description = "当需要生成色卡的时候,请使用它,输入的参数是一个十六进制的 RGB 颜色,比如 #FFFFFF,输出一个RGB颜色数组。"
# description = "色卡或者色板生成的任务是请使用它,输入的参数是一个十六进制的 RGB 颜色,如果输入不是十六进制的 RGB 颜色,需要先转换成十六进制 RGB 颜色再使用该工具"
# 任务是色卡或者色板生成时请使用该工具,输入需要是一个十六进制的颜色值,请先转换成十六进制格式后再调用。
#
description = "Use this tool when the task is to generate a color palette. The input should be a hexadecimal color value or the color name or descirption"
return_direct = True
def _run(self, rgb_hex: str, run_manager: Optional[CallbackManagerForToolRun] = None) -> str:
r, g, b = hex_to_rgb(rgb_hex)
hls = colorsys.rgb_to_hls(r / 255.0, g / 255.0, b / 255.0)
pallet = generate_color_scale(hls, 0.02, 0.2, 0.1)
pallet = [rgbscale_to_hex(x) for x in pallet]
# return json.dumps(pallet)
return ' '.join(pallet)
async def _arun(self, url: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None, ) -> str:
raise NotImplementedError("暂不支持异步")
前端
前端使用 Marked 实现了 Markdown 的解析,这次也加上了 颜色解析(颜色和背景是同一个颜色,选中之后就可以看到了)。
Vue 和 TS 代码,大家也不爱看就不贴了。
后记
说话间,又接到了个活:要负责公司 Gitlab 账号审批创建,继续开搞,明天见。

