青训营X豆包MarsCode 技术训练营第五课 | 鲜花网络电商人脉工具开发实践总结

62 阅读14分钟

下面我们使用LangChain开发一款鲜花网络电商人脉工具,并详细记录开发实践的过程。

1、项目需求

项目背景:易速鲜花电商网络自从创建以来,通过微信、抖音、小红书等自媒体宣传推广,短期内获得了广泛流量展示。目前,营销部门希望以此为契机,再接再厉,继续扩大品牌影响力。经过调研,发现很多用户会通过微博热搜推荐的新闻来购买鲜花赠送给明星、达人等,因此各部门一致认为应该联络相关微博大V,共同推广,带动品牌成长。

然而,发掘并选择适合于“鲜花推广”的微博大V有一定难度。营销部门员工表示,这个任务比找微信、抖音和小红书达人要难得多。他们都希望技术部门能够给出一个“人脉搜索工具”来协助完成这一目标。

项目目标: 帮助市场营销部门的员工找到微博上适合做鲜花推广的大V,并给出具体的联络方案。

这个项目的具体技术实现细节,这里简述如下。

第一步: 通过LangChain的搜索工具,以模糊搜索的方式,帮助运营人员找到微博中有可能对相关鲜花推广感兴趣的大V(比如喜欢玫瑰花的大V),并返回UID。

第二步: 根据微博UID,通过爬虫工具拿到相关大V的微博公开信息,并以JSON格式返回大V的数据。

第三步: 通过LangChain调用LLM,通过LLM的总结整理以及生成功能,根据大V的个人信息,写一篇热情洋溢的介绍型文章,谋求与该大V的合作。

第四步: 把LangChain输出解析功能加入进来,让LLM生成可以嵌入提示模板的格式化数据结构。

第五步: 添加HTML、CSS,并用Flask创建一个App,在网络上部署及发布这个鲜花电商人脉工具,供市场营销部门的人员使用。

在上面的5个步骤中,我们使用到很多LangChain技术,包括提示工程、模型、链、代理、输出解析等。

2、找到目标大V的UID

主程序findbigV.py在第一步完成之后,是这样的。

# 设置OpenAI API密钥
import os
os.environ["OPENAI_API_KEY"] = ''
os.environ["SERPAPI_API_KEY"] = ''# 导入所取的库
import re
from agents.weibo_agent import lookup_V
​
if __name__ == "__main__":
​
    # 拿到UID
    response_UID = lookup_V(flower_type = "牡丹" )
    print(response_UID)
​
    # 抽取UID里面的数字
    UID = re.findall(r'\d+', response_UID)[0]
    print("这位鲜花大V的微博ID是", UID)

返回的是喜欢牡丹的大V的UID

那么文件agents\weibo_agent.py中的lookup_V函数是如何实现这个搜寻UID的功能的呢?

# 导入一个搜索UID的工具
from tools.search_tool import get_UID
​
# 导入所需的库
from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.agents import initialize_agent, Tool
from langchain.agents import AgentType
​
# 通过LangChain代理找到UID的函数
def lookup_V(flower_type: str) :
    # 初始化大模型
    llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")
​
    # 寻找UID的模板
    template = """given the {flower} I want you to get a related 微博 UID.
                  Your answer should contain only a UID.
                  The URL always starts with https://weibo.com/u/
                  for example, if https://weibo.com/u/1669879400 is her 微博, then 1669879400 is her UID
                  This is only the example don't give me this, but the actual UID"""
    # 完整的提示模板
    prompt_template = PromptTemplate(
        input_variables=["flower"], template=template
    )
​
    # 代理的工具
    tools = [
        Tool(
            name="Crawl Google for 微博 page",
            func=get_UID,
            description="useful for when you need get the 微博 UID",
        )
    ]
​
    # 初始化代理
    agent = initialize_agent(
        tools, 
        llm, 
        agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, 
        verbose=True
    )
​
    # 返回找到的UID
    ID = agent.run(prompt_template.format_prompt(flower=flower_type))
​
    return ID

这段代码的目的,是为了通过提供的花的类型(flower type)来查找与之相关的微博UID。其中使用了LangChain中的代理和工具。

这里有两点需要特别说明:

  1. 搜索UID的工具通过from tools.search_tool import get_UID导入。
  2. 提示模板说明,强调了需要的是UID,而不是URL。刚才说了,这是因为后续的爬虫工具需要一个特定的UID,来获取该微博大V的个人信息(公开)。然后我们会继续利用这些信息让LLM为我们写“勾搭”文案。

那么如何使用SerpAPI来实现具体的搜索大V的UID呢?

from langchain.utilities import SerpAPIWrapper
​
def get_UID(flower: str):
    """Searches for Linkedin or twitter Profile Page."""
    search = SerpAPIWrapper()
    res = search.run(f"{flower}")
    return res

运行结果如下所示

img

注意到返回的微博ID是错误的,如果我们返回URL,可以使得大模型提炼出UID。

这里我们可以继承SerpAPIWrapper类,并构造一个CustomSerpAPIWrapper类,在这个类中,我们重构_process_response这个静态方法。

新的search_tool.py完整代码如下:

# 导入SerpAPIWrapper
from langchain.utilities import SerpAPIWrapper
​
# 重新定制SerpAPIWrapper,重构_process_response,返回URL
class CustomSerpAPIWrapper(SerpAPIWrapper):
    def __init__(self):
        super(CustomSerpAPIWrapper, self).__init__()
​
    @staticmethod
    def _process_response(res: dict) -> str:
        """Process response from SerpAPI."""
        if "error" in res.keys():
            raise ValueError(f"Got error from SerpAPI: {res['error']}")
        if "answer_box_list" in res.keys():
            res["answer_box"] = res["answer_box_list"]
        '''删去很多无关代码'''
        snippets = []
        if "knowledge_graph" in res.keys():
            knowledge_graph = res["knowledge_graph"]
            title = knowledge_graph["title"] if "title" in knowledge_graph else ""
            if "description" in knowledge_graph.keys():
                snippets.append(knowledge_graph["description"])
            for key, value in knowledge_graph.items():
                if (
                    isinstance(key, str)
                    and isinstance(value, str)
                    and key not in ["title", "description"]
                    and not key.endswith("_stick")
                    and not key.endswith("_link")
                    and not value.startswith("http")
                ):
                    snippets.append(f"{title} {key}: {value}.")
        if "organic_results" in res.keys():
            first_organic_result = res["organic_results"][0]
            if "snippet" in first_organic_result.keys():
                # 此处是关键修改
                # snippets.append(first_organic_result["snippet"])
                snippets.append(first_organic_result["link"])                
            elif "snippet_highlighted_words" in first_organic_result.keys():
                snippets.append(first_organic_result["snippet_highlighted_words"])
            elif "rich_snippet" in first_organic_result.keys():
                snippets.append(first_organic_result["rich_snippet"])
            elif "rich_snippet_table" in first_organic_result.keys():
                snippets.append(first_organic_result["rich_snippet_table"])
            elif "link" in first_organic_result.keys():
                snippets.append(first_organic_result["link"])
        if "buying_guide" in res.keys():
            snippets.append(res["buying_guide"])
        if "local_results" in res.keys() and "places" in res["local_results"].keys():
            snippets.append(res["local_results"]["places"])
​
        if len(snippets) > 0:
            return str(snippets)
        else:
            return "No good search result found"# 获取与某种鲜花相关的微博UID的函数
def get_UID(flower: str):
    """Searches for Linkedin or twitter Profile Page."""
    # search = SerpAPIWrapper()
    search = CustomSerpAPIWrapper()
    res = search.run(f"{flower}")
    return res

唯一的区别就是,我们在下面的逻辑中返回了link,而不是snippet。

snippets.append(first_organic_result["link"])

此时运行主程序findbigV.py,会发现代理中返回了URL信息,并且经过进一步思考,提炼出了UID。

成功得到了大 V 的 UID

3、爬取目标UID的资料

在找到UID之后,就可以爬取该大V的资料了。

    # 根据UID爬取大V信息
    person_info = get_data(UID)
    print(person_info)

get_data方法中的业务逻辑如下所示

# 导入所需的库
import json
import requests
import time
​
# 定义爬取微博用户信息的函数
def scrape_weibo(url: str):
    '''爬取相关鲜花服务商的资料'''
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36",
        "Referer": "https://weibo.com"
    }
    cookies = {你的cookies}
    response = requests.get(url, headers=headers, cookies=cookies)
    time.sleep(3)   # 加上3s 的延时防止被反爬
    return response.text
​
# 根据UID构建URL爬取信息
def get_data(id):
    url = "https://weibo.com/ajax/profile/detail?uid={}".format(id)
    html = scrape_weibo(url)
    response = json.loads(html)
​
    return response

从我的浏览器中获取 Cookie,以下是简单步骤:

  1. 使用浏览器(如 Chrome、Firefox)访问微博并登录。
  2. 登录后,右键单击页面并选择“检查”(Inspect)。
  3. 打开开发者工具,点击 Network 选项卡。
  4. 在页面上进行一些操作(如刷新页面),然后在 Network 选项卡下查看请求列表。
  5. 选择任一请求项,然后在右侧的 Headers 选项卡中查找 Request Headers 部分。
  6. 在这部分中,你应该可以看到一个名为 Cookie 的字段,这就是你需要的 Cookie 值。

img

此时,运行 findbigV.py,就得到了下面的输出。

img

我们对上述输出进行精简,删除没有意义的部分

import re
​
def contains_chinese(s):
    return bool(re.search('[\u4e00-\u9fa5]', s))
​
def remove_non_chinese_fields(d):
    if isinstance(d, dict):
        to_remove = [key for key, value in d.items() if isinstance(value, (str, int, float, bool)) and (not contains_chinese(str(value)))]
        for key in to_remove:
            del d[key]
        
        for key, value in d.items():
            if isinstance(value, (dict, list)):
                remove_non_chinese_fields(value)
    elif isinstance(d, list):
        to_remove_indices = []
        for i, item in enumerate(d):
            if isinstance(item, (str, int, float, bool)) and (not contains_chinese(str(item))):
                to_remove_indices.append(i)
            else:
                remove_non_chinese_fields(item)
        
        for index in reversed(to_remove_indices):
            d.pop(index)

在findbigV.py中,调用这个函数,对爬虫的输出结果进行了精简。

    # 移除无用的信息
    remove_non_chinese_fields(person_info)
    print(person_info)

重新运行findbigV.py,结果如下:

img

此时,爬取的内容就只剩下了干货。

4、生成总结文章

把上一步中返回的JSON数据(大V的个人简介)传递给LLM,发挥大模型超强的总结整理和文本生成能力,帮助运营人员创建文案。

这个文案可以有很多种形式,比如说可以总结一下大V的特点,根据他的自我介绍猜测一下他的兴趣爱好,还可以让LLM帮助运营人员撰写一篇联络信件的草稿。

这就看我们如何设计提示模板了。

重构之后的 findbigV.py 代码如下:

# 设置OpenAI API密钥
import os
os.environ["OPENAI_API_KEY"] = 'Your OpenAI Key'
os.environ["SERPAPI_API_KEY"] = 'Your SerpAPI Key'# 导入所取的库
import re
from agents.weibo_agent import lookup_V
from tools.general_tool import remove_non_chinese_fields
from tools.scraping_tool import get_data
from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.chains import LLMChain
​
if __name__ == "__main__":
​
    # 拿到UID
    response_UID = lookup_V(flower_type = "牡丹" )
​
    # 抽取UID里面的数字
    UID = re.findall(r'\d+', response_UID)[0]
    print("这位鲜花大V的微博ID是", UID)
​
    # 根据UID爬取大V信息
    person_info = get_data(UID)
    print(person_info)
​
    # 移除无用的信息
    remove_non_chinese_fields(person_info)
    print(person_info)
​
    # 设计提示模板
    letter_template = """
         下面是这个人的微博信息 {information}
         请你帮我:
         1. 写一个简单的总结
         2. 挑两件有趣的事情说一说
         3. 找一些他比较感兴趣的事情
         4. 写一篇热情洋溢的介绍信
     """ 
    prompt_template = PromptTemplate(
        input_variables=["information"],
        template=letter_template
    )
​
    # 初始化大模型
    llm = ChatOpenAI(model_name="gpt-3.5-turbo")    
​
    # 初始化链
    chain = LLMChain(llm=llm, prompt=prompt_template)
​
    # 生成文案
    result = chain.run(information = person_info)
    print(result)

运行程序之后,LLM没有让我们失望,给出了相当专业的文案。

img

下面我整理一下程序,把生成文案的功能放在 \tools\textgen_tool.py 中,定义为 generate_letter 函数。这样主程序显得比较清爽。

新的 findbigV.py 代码如下:

# 导入所取的库
import re
from agents.weibo_agent import lookup_V
from tools.general_tool import remove_non_chinese_fields
from tools.scraping_tool import get_data
from tools.textgen_tool import generate_letter
​
​
​
if __name__ == "__main__":
​
    # 拿到UID
    response_UID = lookup_V(flower_type = "牡丹" )
​
    # 抽取UID里面的数字
    UID = re.findall(r'\d+', response_UID)[0]
    print("这位鲜花大V的微博ID是", UID)
​
    # 根据UID爬取大V信息
    person_info = get_data(UID)
    print(person_info)
​
    # 移除无用的信息
    remove_non_chinese_fields(person_info)
    print(person_info)
​
    # 调用函数根据大V信息生成文本
    result = generate_letter(information = person_info)
    print(result)

5、加入输出解析

上面的文案已经非常到位,但是需要把文字Copy Paste出来才能够使用。下面,我们要通过LangChain的输出解析器一步到位,让LLM给我们生成有良好结构的JSON文档,便于下一步集成到 HTML 中进行展示。

在 tools 文件夹中,新建一个 tools\ParsingTool.py 文件。

# 导入所需的类
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from typing import List# 定义一个名为TextParsing的模型,描述了如何解析大V信息
class TextParsing(BaseModel):
    summary: str = Field(description="大V个人简介")  # 大V的简介或背景信息
    facts: List[str] = Field(description="大V的特点")  # 大V的一些显著特点或者事实
    interest: List[str] = Field(description="这个大V可能感兴趣的事情")  # 大V可能感兴趣的主题或活动
    letter: List[str] = Field(description="一篇联络这个大V的邮件")  # 联络大V的建议邮件内容
​
    # 将模型对象转换为字典
    def to_dict(self):
        return {
            "summary": self.summary,
            "facts": self.facts,
            "interest": self.interest,  
            "letter": self.letter,    
        }
​
# 创建一个基于Pydantic模型的解析器,用于将文本输出解析为特定的结构
letter_parser: PydanticOutputParser = PydanticOutputParser(
    pydantic_object=TextParsing
)

此处,TextParsing 是一个用 Pydantic 定义的数据模型,描述了一个大V的个人信息如何被解析和组织。该模型包含四个字段:summary(简介)、facts(事实)、interest(兴趣)、letter(信件)。而to_dict是一个实例方法,它可以将该模型的实例转换为一个字典。最后,我们创建了一个PydanticOutputParser对象,该对象基于TextParsing模型,可以被用来解析一段文本并填充到这个数据模型中。

然后,我们更新 \tools\textgen_tool.py 文件。

# 导入所需要的库
from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.chains import LLMChain
from tools.parsing_tool import letter_parser
​
​
​
# 生成文案的函数
def generate_letter(information):
​
    # 设计提示模板
    letter_template = """
         下面是这个人的微博信息 {information}
         请你帮我:
         1. 写一个简单的总结
         2. 挑两件有趣的特点说一说
         3. 找一些他比较感兴趣的事情
         4. 写一篇热情洋溢的介绍信
         \n{format_instructions}"""
    
    prompt_template = PromptTemplate(
        input_variables=["information"],
        template=letter_template,
        partial_variables={
            "format_instructions": letter_parser.get_format_instructions()
        },         
    )
​
    # 初始化大模型
    llm = ChatOpenAI(model_name="gpt-3.5-turbo")    
​
    # 初始化链
    chain = LLMChain(llm=llm, prompt=prompt_template)
​
    # 生成文案
    result = chain.run(information = information)
    return result

通过 {format_instructions} 和 partial_variables 参数,我们利用输出解析器增强了这个提示模板,让 LLM 直接返回我们所需要的格式。

重新运行 findbigV.py,可以看到,输出已经被解析为标准的JSON格式。

img

不过,此时的文案似乎和鲜花运营缺少了一些关联,你可以尝试着调整提示模板中的内容,让这封信写得更加贴合我们鲜花运营的具体意图。

6、部署前端工具

下面,我们就制作一个前端页面,同时把这个工具部署到服务器上面去,让我们的运营人员能够随时访问它。

HTML 文件

首先,创建一个 HTML 文件,用于交互展示,这个文件放在 templates 目录下。

<!-- templates/index.html --><!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <title>Ice Breaker</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.0/css/all.min.css" />
    <div class="spinner-container" id="spinner-container" style="display: none;">
        <i id="loading-spinner" class="fas fa-spinner fa-spin"></i>
    </div>
</head>
<body>
    <div class="container">
        <h1>易速鲜花人脉工具</h1>
        <form id="name-form">
            <input type="text" id="flower" name="flower" placeholder="输入一种花(或者其它东西也行)">
            <button id="magic-button" type="submit">找到大V</button>
        </form>
        <div id="result">
            <img id="profile-pic" src="" alt="Profile Picture" style="display: none; max-width: 100%; height: auto; border-radius: 50%; margin-bottom: 20px;">
            <h2>基本情况</h2>
            <p id="summary"></p>
            <h2>特色内容</h2>
            <div id="facts"></div>
            <h2>可能感兴趣的事儿</h2>
            <div id="interest"></div>
            <h2>联络邮件</h2>
            <div id="letter"></div>
        </div>
    </div>
    <script>
        $(document).ready(function () {
            $('#name-form').on('submit', function (e) {
                e.preventDefault();
                $('#spinner-container').show();
                $.ajax({
                    url: '/process',
                    data: $('#name-form').serialize(),
                    type: 'POST',
                    success: function (response) {
                        $('#profile-pic').attr('src', '你的URL'); 
                        $('#profile-pic').show(); 
                        $('#summary').text(response.summary);
                        $('#facts').html('<ul>' + response.facts.map(fact => '<li>' + fact + '</li>').join('') + '</ul>');
                        $('#interest').html('<ul>' + response.interest.map(interest => '<li>' + interest + '</li>').join('') + '</ul>');
                        $('#letter').text(response.letter);
                    },
                    error: function (error) {
                        console.log(error);
                    },
                    complete: function () {
                        $('#spinner-container').hide();
                    }
                });
            });
        });
    </script>
</body>
</html>

CSS 文件

为了让 HTML 美一点,我们还制作了一个 CSS 文件 style.css,放在 \static\css\ 目录下。

img

这个文件就请你去咱们的 GitHub Repo 下载,我就不在这儿展示了。

重构 findbigV.py

下一步是重构 findbigV.py,把功能封装到一个函数中。

def find_bigV(flower: str) :
    # 拿到UID
    response_UID = lookup_V(flower_type = flower )
​
    # 抽取UID里面的数字
    UID = re.findall(r'\d+', response_UID)[0]
    print("这位鲜花大V的微博ID是", UID)
​
    # 根据UID爬取大V信息
    person_info = get_data(UID)
    print(person_info)
​
    # 移除无用的信息
    remove_non_chinese_fields(person_info)
    print(person_info)
​
    # 调用函数根据大V信息生成文本
    result = generate_letter(information = person_info)
    print(result)
​
    return result

创建 app.py

下面,我们创建一个基于 Flask 的 Web应用,主要用于显示一个输入表单,供大家提交花的名称,并返回市场营销人员所需要的内容,在网页中展示。

# 导入所需的库和模块
from flask import Flask, render_template, request, jsonify
from findbigV import find_bigV
import json
​
# 实例化Flask应用
app = Flask(__name__)
​
# 主页路由,返回index.html模板
@app.route("/")
def index():
    return render_template("index.html")
​
# 处理请求的路由,仅允许POST请求
@app.route("/process", methods=["POST"])
def process():
    # 获取提交的花的名称
    flower = request.form["flower"]
    # 使用find_bigV函数获取相关数据
    response_str = find_bigV(flower=flower)
    # 使用json.loads将字符串解析为字典
    response = json.loads(response_str)
​
    # 返回数据的json响应
    return jsonify(
        {
            "summary": response["summary"],
            "facts": response["facts"],
            "interest": response["interest"],
            "letter": response["letter"],
        }
    )    
​
# 判断是否是主程序运行,并设置Flask应用的host和debug模式
if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)

程序非常简单,简单介绍下。

  1. 导入库和模块:这部分导入了Flask框架的相关模块,以及find_bigV函数和JSON库。

  2. 创建了一个Flask应用实例。

  3. 定义主页路由:当用户访问这个路由时,它将返回一个叫 index.html 的模板。

  4. 定义处理请求的路由:这是一个专门处理 POST 请求的路由。其流程如下:

    • 从表单数据中获取名为 "flower" 的字段
    • 使用 find_bigV 函数查询与该花名相关的数据
    • 解析返回的数据(字符串格式)为 Python 字典
    • 将这些数据整理并返回为 JSON 格式的响应
  5. 启动 Flask 应用,监听所有公开的 IP 地址,并在调试模式中运行。

这样,我们就真正大功告成了,系统上线,运营人员可以在网页中调用我们的产品了。

运行App程序,就看到了这个人脉工具。

img