Itou86
发布于

记录一下从 0 开始编写第一个 ACT

1 确定需求

1.1 想做什么

首先,根据半个宇宙的社区属性与方向,确定了开发的 ACT 最好需要贴近日常,于是根据我的一些生活习惯,决定实现「收集分析百度上的种草数据」功能。粗略浏览了一下社区,发现目前没有完全和我想法类似的 ACT,那么就以这个功能为核心开始着手开发。

在浏览社区的过程中,发现在 ACT 的具体开发上并没有很详细的说明或记录,那么就以此文来抛砖引玉吧,为社区发展尽一份力,也欢迎大家提供反馈和建议。

1.2 做到什么

考虑到单纯「种草」的范围太大了,于是给定一个 1.0 版本的支持搜索范围,并以此作为测试用例:

  1. 实现搜索:「深圳的美食」,并返回种草排行榜。
  2. 实现搜索:「打工人好物」,并返回种草排行榜。

2 前置准备

2.1 开发环境

文档中提到,ACT 基本可以使用 Python 语言开发,同时 kOS 的控制台具有代码运行与测试的功能,因此仅使用 VS Code 来提高本地代码的可读性,不考虑代码的编译环境。

2.2 AI 工具

由于我 没有 Python 语言的开发经验(完全没用过) ,所以为了提高效率,使用了一些 AI 工具。包括但不限于:

  1. 向 AI 提问一些简单的基础语法。
  2. 在群里要了一份开发手册的 PDF 版本,丢进 Kimi 大模型中解析然后提问。
  3. 在 VS Code 中配置「通义灵码」插件。

多管齐下协助开发。

3 功能实现

3.1 需求拆解

在我看来,对于这样的大模型应用层开发,完全可以仅把大模型当作分析文本的黑盒:只需输入需要分析的文本数据,并给定一些参数,他就能返回处理的结果。

因此,我们只需要将需求的各个功能抽象成不同模块,然后结合标准 ACT 的功能实现,将模块拆分为实现的步骤,即可完成需求拆解(其实这里也可以丢给 AI 来分解,但是当时忘了)。

这里我们直接一步到位,将需求拆分成这几个步骤:

  1. 获取用户输入内容
  2. 解析用户输入内容并拆分为元数据
  3. 将元数据分别进行网络搜索
  4. 整理搜索结果
  5. 语义分析
  6. 返回结果给用户
  7. 记录日志

3.2 具体实现

3.2.1 获取用户输入内容

直接参考官方给出的 ACT 模板:

# 获取ACT触发启动时的消息
    query = nact.k_get_act_query()
    if not query:
        query = nact.k_ask_for_answer('请输入想种草的内容')  # 如果没有查询要求,请求用户输入

3.2.2 解析用户输入内容并拆分为元数据

这一步是需求中的一个难点,也是此 ACT 与直接使用「搜一搜」不同的地方。这里我参考了开发者「可道玩AI」的「星伴 ACT:代表作」里的代表作搜索结果优化思路,对用户的提问内容做进一步加工。

这里暂时直接使用 nact.k_semantic_rephrase 来对语义进行转换,后续会更新更好的方案。

考虑了两个方案如下:

  1. 递归调用上一次重写的结果,也就是将上一次重写的结果作为上下文填入函数的参数中。
  2. 分别以「缩写」和「扩写」为要求重写两次,最终分别查询三次结果。

方案一

search_list = [query]
count = 0
while count < 3:
    if count != 0:
        # 如果不是第一次循环,则在函数中加入上下文
        search = nact.k_semantic_rephrase('在不影响本意的情况下重写这个问题', query, f'参考:{search}', '')
    else:
        # 第一次循环则直接调用函数
        search = nact.k_semantic_rephrase('在不影响本意的情况下重写这个问题', query, '', '')
    search_list.append(search)
    count += 1
return search_list

测试结果如下:

方案二

search_list = [query]
search = nact.k_semantic_rephrase('在不影响本意的情况下缩写这个问题', query, '', '')
search_list.append(search)
search = nact.k_semantic_rephrase('在不影响本意的情况下扩写这个问题', query, '', '')
search_list.append(search)
return search_list

测试结果如下:

使用搜索引擎分别测试搜索结果后,最终确定使用方案二作为该版本的实现方案,感兴趣的可以分别去搜索一下,这里就不放图了。

在多次测试方案后,进一步更新语义重写的「改写要求」参数。同时,在参考官方的「搜一搜」ACT 模板时,发现代码在处理搜索结果时使用了 KOSConcurrentBatcher 函数,因此更新方案代码,加入批处理,优化了该模块的运行时间

更新代码如下:

# 创建批处理器
batcher: nact.KOSConcurrentBatcher = nact.k_concurrent_create_batcher()
# 方案二:
batcher.submit(nact.k_semantic_rephrase, '在不影响本意的情况下缩写这个问题', query, '', '')
batcher.submit(nact.k_semantic_rephrase, '在不影响本意的情况下,以提问的方式扩写这个问题', query, '', '')
    # 等待批处理任务完成
results: list = batcher.wait()
for result in results:
    if not result:
        continue
    # 获取批处理任务的结果
    search_list.append(result)
return search_list

3.2.3 将元数据分别进行网络搜索

这里也是需求的难点之一,好在官方提供的「搜一搜」ACT 中已经有了完整的实现,照抄简单改一改即可。

部分代码如下:

# 创建批处理器
batcher: nact.KOSConcurrentBatcher = nact.k_concurrent_create_batcher()
tl_all = []

# 遍历问题,执行网络搜索
search_count = 0
for search in search_list:
    tl = nact.k_web_search(search)
    if not tl: # 没有搜索结果则跳过
        search_count += 1
        continue
    for idx, topic in enumerate(tl): # 有结果则遍历搜索结果
        tl_all.append(tl) # 将搜索结果添加到列表
        batcher.submit(handle_topic, query, topic, idx) # 提交处理任务到批处理器

# 如果所有搜索都未搜索到结果则结束
if search_count == len(search_list):
    nact.k_message_send('未搜索到相关内容!')
    return None

# 等待批处理任务完成
results: list = batcher.wait()

handle_topic 函数如下:

# 定义处理单个搜索结果主题的函数
def handle_topic(query: str, topic: nact.KOSWebSearchTopic, idx: int) -> Tuple[nact.KOSWebPageData, str, str]:
    # 获取网页数据
    page_data: nact.KOSWebPageData = nact.k_web_get_page_data(topic.source_url)
    if not page_data or not page_data.content:
        return None, '', '' # 如果没有获取到数据或内容,返回空值

    # 设置标题,如果主题没有标题,则使用网页数据的标题
    title = topic.title if topic.title else page_data.title
    if not title: # 如果还是没有标题
        title = str(nact.k_time_cur_milliseconds()) + str(idx) # 使用当前时间毫秒数和索引作为标题
        ref_tag = page_data.domain # 使用页面域名作为引用标签
    else:
        title = title.replace('/', '') # 替换掉标题中的斜杠
        ref_tag = title # 标题作为引用标签

    page_data.title = title # 更新网页数据的标题

    # 对页面内容进行语义总结,生成摘要
    summary = nact.k_semantic_summarize_answer(page_data.content, query)
    return page_data, ref_tag, summary # 返回网页数据、引用标签和摘要

3.2.4 整理搜索结果

照抄后简单修改:

# 初始化引用源对象和摘要字符串
ref_source = nact.KOSMsgWebContentRefSource()
all_summary = ''

# 遍历批处理结果
for result in results:
    if not result:
        continue
    (page_data, ref_tag, summary) = result
    if not page_data:
        continue
    # 添加引用网页到引用源对象
    ref_source.add_web_ref(ref_tag, page_data.url)
    all_summary += summary + "\n" # 将摘要追加到字符串

3.2.5 语义分析

照抄:

# 对所有摘要进行语义总结
summary = nact.k_semantic_summarize_answer(all_summary, query)
if not summary:
    nact.k_message_send('未搜索总结出有效内容') # 如果没有总结内容,发送消息告知用户
    return

3.2.6 返回结果给用户

发送结果:

# 发送总结后的内容给用户
nact.k_message_send(summary, ref_source)

3.2.7 记录日志

遍历搜索结果,打印日志信息,保存记录:

# 打印日志信息
nact.k_print('开始记录种草结果')

# 再次遍历批处理结果,提交记录任务到批处理器
for idx, result in enumerate(results):
    if not result:
        continue
    (page_data, ref_tag, summary) = result
    if not page_data:
        continue
    batcher.submit(record_topic, all_tl[idx], page_data) # 提交记录主题的任务

# 等待记录任务完成
batcher.wait()

record_topic 函数如下:

# 定义记录搜索结果主题的函数
def record_topic(topic: nact.KOSWebSearchTopic, page_data: nact.KOSWebPageData):
    # 获取当前本地日期字符串
    time_str = nact.k_time_cur_local_day_str()
    if nact.k_file_is_available():  # 检查文件系统是否可用
        # 打开文件,准备将内容写入,如果文件存在则覆盖
        file = nact.k_except_wrapper(nact.k_file_open, f'/种草/{time_str}_{page_data.title}.txt', nact.FileOpenMode.OVERWRITE,
                                    source=nact.FileSource.WEB_LINK, source_url=topic.source_url)
        if file:  # 如果成功打开文件
            nact.k_file_append(file, page_data.content)  # 将页面内容追加到文件

4 测试与代码

4.1 测试

分别测试直接询问与使用 ACT询问,结果如下:


直接搜索


直接搜索


使用 ACT


使用 ACT


使用 ACT

4.2 代码

完整代码与详细注释如下:

# -*- coding: utf-8 -*-
from typing import Tuple # 导入typing模块,用于类型注解
from kOS import nact # 导入kOS系统的nact模块,提供了一系列原生ACT函数

# 定义输入内容解析函数
def handle_search(query: str) -> Tuple[str, ...]:
    search_list = [query]
    # 创建批处理器
    batcher: nact.KOSConcurrentBatcher = nact.k_concurrent_create_batcher()

    # 方案一:以递归方式调用上一次重写的结果
    '''
    count = 0
    while count < 3:
        if count != 0:
            # 如果不是第一次循环,则在函数中加入上下文
            search = nact.k_semantic_rephrase('在不影响本意的情况下,以提问的方式重写这个问题', query, f'参考:{search}', '')
        else:
            # 第一次循环则直接调用函数
            search = nact.k_semantic_rephrase('在不影响本意的情况下,以提问的方式重写这个问题', query, '', '')
        search_list.append(search)
        count += 1
    '''

    # 方案二:调用两次,分别为缩写与扩写
    batcher.submit(nact.k_semantic_rephrase, '在不影响本意的情况下缩写这个问题', query, '', '')
    batcher.submit(nact.k_semantic_rephrase, '在不影响本意的情况下,以提问的方式扩写这个问题', query, '', '')

    # 等待批处理任务完成
    results: list = batcher.wait()

    for result in results:
        if not result:
            continue
        
        search_list.append(result) # 获取批处理任务的结果

    return search_list

# 定义处理单个搜索结果主题的函数
def handle_topic(query: str, topic: nact.KOSWebSearchTopic, idx: int) -> Tuple[nact.KOSWebPageData, str, str]:
    # 获取网页数据
    page_data: nact.KOSWebPageData = nact.k_web_get_page_data(topic.source_url)
    if not page_data or not page_data.content:
        return None, '', '' # 如果没有获取到数据或内容,返回空值

    # 设置标题,如果主题没有标题,则使用网页数据的标题
    title = topic.title if topic.title else page_data.title
    if not title: # 如果还是没有标题
        title = str(nact.k_time_cur_milliseconds()) + str(idx) # 使用当前时间毫秒数和索引作为标题
        ref_tag = page_data.domain # 使用页面域名作为引用标签
    else:
        title = title.replace('/', '') # 替换掉标题中的斜杠
        ref_tag = title # 标题作为引用标签

    page_data.title = title # 更新网页数据的标题

    # 对页面内容进行语义总结,生成摘要
    summary = nact.k_semantic_summarize_answer(page_data.content, query)
    return page_data, ref_tag, summary # 返回网页数据、引用标签和摘要

# 定义记录搜索结果主题的函数
def record_topic(topic: nact.KOSWebSearchTopic, page_data: nact.KOSWebPageData):
    # 获取当前本地日期字符串
    time_str = nact.k_time_cur_local_day_str()
    if nact.k_file_is_available():  # 检查文件系统是否可用
        # 打开文件,准备将内容写入,如果文件存在则覆盖
        file = nact.k_except_wrapper(nact.k_file_open, f'/种草/{time_str}_{page_data.title}.txt', nact.FileOpenMode.OVERWRITE,
                                    source=nact.FileSource.WEB_LINK, source_url=topic.source_url)
        if file:  # 如果成功打开文件
            nact.k_file_append(file, page_data.content)  # 将页面内容追加到文件

def main():
    # 获取ACT触发启动时的消息
    query = nact.k_get_act_query()
    if not query:
        query = nact.k_ask_for_answer('请输入想种草的内容') # 如果没有查询要求,请求用户输入

    # 分析查询要求
    search_list = handle_search(query)
    # 检查分析结果
    # nact.k_message_send('分析结果:' + str(search_list))

    # 创建批处理器
    batcher: nact.KOSConcurrentBatcher = nact.k_concurrent_create_batcher()
    all_tl = []

    # 遍历问题,执行网络搜索
    search_count = 0
    for search in search_list:
        tl = nact.k_web_search(search)
        if not tl: # 没有搜索结果则跳过
            search_count += 1
            continue
        for idx, topic in enumerate(tl): # 有结果则遍历搜索结果
            all_tl.append(tl) # 将搜索结果添加到列表
            batcher.submit(handle_topic, query, topic, idx) # 提交处理任务到批处理器

    # 如果所有搜索都未搜索到结果则结束
    if search_count == len(search_list):
        nact.k_message_send('未搜索到相关内容!')
        return None

    # 等待批处理任务完成
    results: list = batcher.wait()

    # 初始化引用源对象和摘要字符串
    ref_source = nact.KOSMsgWebContentRefSource()
    all_summary = ''

    # 遍历批处理结果
    for result in results:
        if not result:
            continue
        (page_data, ref_tag, summary) = result
        if not page_data:
            continue
        # 添加引用网页到引用源对象
        ref_source.add_web_ref(ref_tag, page_data.url)
        all_summary += summary + "\n" # 将摘要追加到字符串

    # 对所有摘要进行语义总结
    summary = nact.k_semantic_summarize_answer(all_summary, query)
    if not summary:
        nact.k_message_send('未搜索总结出有效内容') # 如果没有总结内容,发送消息告知用户
        return

    # 发送总结后的内容给用户
    nact.k_message_send(summary, ref_source)

    # 打印日志信息
    nact.k_print('开始记录种草结果')

    # 再次遍历批处理结果,提交记录任务到批处理器
    for idx, result in enumerate(results):
        if not result:
            continue
        (page_data, ref_tag, summary) = result
        if not page_data:
            continue
        batcher.submit(record_topic, all_tl[idx], page_data) # 提交记录主题的任务

    # 等待记录任务完成
    batcher.wait()

# 调用主函数执行
main()

4 小结

  1. 代码写完后才最终确定了 ACT 名,叫「种草」。
  2. 原本是想要收集小红书上的数据再处理,但考虑到法律风险与技术难度,决定还是先从 ACT 默认的搜索入手。
  3. 在难点的解析输入内容里,原本是想先拆分再多次组合,但尝试了几次后决定还是先以简单实现为主,后续有想法再更新。
  4. 实际运行速度非常慢,实现的效果和我想的也有差距,甚至说相比直接使用搜一搜几乎没有提升。 因此此 ACT 目前仅作学习用途。或许未来会更新?
浏览 (943)
点赞 (11)
收藏
2条评论
银河领主
银河领主
犀利啊,没有python基础都已经写的这么6了,强烈点赞~ 关于慢的问题我解释一下哈: * 看起来总结是延用搜一搜的总结逻辑哈,这样的话总结其实分为了两步:页面单独总结 + 最后的汇总总结。页面的单独总结通过batcher并发提速,但因为上面是搜索到了多个结果,大概是6个左右,这超出了我们对单个ACT的线程池限制(这块我们后面也会扩大的),这样实际上并发提速还是相当于两次的总结时间,加上最后的汇总,实际上产生了3次总结的时间 * 按理我们的大模型单次总结时间应该在10几秒左右,但我看有不少大网页,此时又会触发我们内部的长文本递归总结,会产生多次大模型交互,所以平均下来这种大网页总结花了30秒左右的时间(如果网页文本太长还会更长),整体光总结就可能90多秒了。 * 搜一搜因为控制了搜索的结果数量,所以基本上不会产生过多的并发 这确实太慢了,但不要急,我们最近有个比较重要的更新:支持搜索问答。你可以参考开发文档哈,nact.k_search_and_answer。这个支持超大文本的搜索问答总结。这里写个非常简单的例子哈: ```python # -*- coding: utf-8 -*- from kOS import nact def main(): query = nact.k_get_act_query() if not query: query = nact.k_ask_for_answer(&#39;请输入想种草的内容&#39;) # 如果没有查询要求,请求用户输入 options: nact.KOSSearchOptions = nact.KOSSearchOptions() options.search_source = nact.SearchSource.INTERNET answer = nact.k_search_and_answer(query, search_options=options) nact.k_message_send(answer) main() ``` k_search_and_answer不仅支持纯网络的搜索,还支持数据舱的搜索以及混合搜索,还能够通过参数控制是简单回答还是深度回答,可以尝试一下哈
点赞 4
评论
Itou86
原来控制了并发数量,怪不得我用上并发但感觉速度提高的不多哈哈哈哈哈,主要疑惑的就是这点,感谢解答。我去试试搜索问答。
点赞
评论
道哥
道哥
很好的一次尝试。看来官方ACT的示范代码作用还是不错的。 kOS正在一次大的升级过程中,欢迎关注更新动态,及时给我们一些反馈建议。
点赞 3
评论
Itou86
好耶
点赞
评论