excel流式生成

无论是easyExcel或者poi等excel处理框架,在生成excel时都需要同步等待操作。但如果数据量较大,生成excel的时间会比较长,对于用户来说需要等待,影响体验。这时候就需要考虑使用流式生成excel。

下面提供了几种方案实现流失生成,能够在页面上直接开启下载。

  1. 后端处理

excel2017版本之后,文件本身其实是一个zip文件,里面包含了多个xml文件,这些xml文件描述了excel的内容。所以我们可以通过流式生成xml文件,然后将这些xml文件打包成zip文件,最后返回给浏览器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public ExcelGenerator(InputStream template, Integer dataIndex, OutputStream outputStream) throws IOException {
this.index = dataIndex;
zos = new ZipOutputStream(outputStream);
try (ZipInputStream zipInputStream = new ZipInputStream(template)) {
ZipEntry ze = null;
String sheet1 = null;
while ((ze = zipInputStream.getNextEntry()) != null) {
if (StringUtils.equals(ze.getName(), "xl/worksheets/sheet1.xml")) {
sheet1 = IOUtils.toString(zipInputStream, StandardCharsets.UTF_8);
} else {
ZipEntry zeOut = new ZipEntry(ze.getName());
zeOut.setSize(ze.getSize());
zeOut.setTime(ze.getTime());
zos.putNextEntry(zeOut);
IOUtils.copy(zipInputStream, zos);
zos.flush();
zos.closeEntry();

}
}
//准备sheet1的数据
//FIXME: 玩弄xml
ZipEntry entry = new ZipEntry("xl/worksheets/sheet1.xml");
zos.putNextEntry(entry);
int sheetDataIndex = sheet1.indexOf("</sheetData>");
zos.write(sheet1.substring(0, sheetDataIndex).getBytes(StandardCharsets.UTF_8));
finishStr = sheet1.substring(sheetDataIndex);
}
}

//只修改sheetData -> row 的数据
public void render(List<T> views) throws IOException {
for (T t : views) {
zos.write(renderRowView(t));
}
zos.flush();
}

private byte[] renderRowView(T t) {
Map<Integer, String> cells = new TreeMap<>();
ReflectionUtils.doWithFields(t.getClass(), f -> {
ColumnMeta meta = f.getAnnotation(ColumnMeta.class);
try {
Object value = PropertyUtils.getProperty(t, f.getName());
ConvertMeta convert = f.getAnnotation(ConvertMeta.class);
if (convert != null) {
ConvertInfo convertInfo = new ConvertInfo();
convertInfo.setDefaultValue(convert.defaultVaule());
convertInfo.setTargetType(f.getType());
convertInfo.setFormat(convert.format());
//FIXME: 性能问题 缓存实例
IConvert instance = convert.convert().newInstance();
instance.setConvertInfo(convertInfo);
value = instance.convert(value);
}
cells.put(meta.index(), value.toString());
} catch (Exception e) {
}
}, f -> null != f.getAnnotation(ColumnMeta.class));
byte[] row = ("<row r=\"" + index + "\">" +
cells.entrySet().stream().map(e -> {
return "<c r=\"" + getColName(e.getKey()) + index + "\" t=\"inlineStr\"><is><t>" + e.getValue() + "</t></is></c>";
}).collect(Collectors.joining("")) + "</row>").getBytes(StandardCharsets.UTF_8);
index++;
return row;
}

private String getColName(Integer index) {
if (index <= 26) {
return String.valueOf((char) ('A' + index - 1));
} else {
return String.valueOf((char) ('A' + index / 26 - 1)) + String.valueOf((char) ('A' + index % 26 - 1));
}
}


public void finish() throws IOException {
//完成sheet1的xml
zos.write(finishStr.getBytes(StandardCharsets.UTF_8));
zos.flush();
zos.closeEntry();
zos.finish();
}
  1. 前端处理

前端可以通过restful接口获取到excel内容,然后通过blob对象生成excel文件,最后通过a标签的download属性下载excel文件。

https://medium.com/@Nopziiemoo/create-excel-files-using-javascript-without-all-the-fuss-2c4aa5377813

Examination

使用dify.ai构建考试问答机器人

发现问卷星没有强制使用https,本地配置hosts

1
127.0.0.1	kaoshi.wjx.top

实现一个代理服务,注入我们的js代码,实现答题时访问dify api 获取答案,将答案拼接在问题下面。将原来的地址改成http访问,也省需要mitm了。
当然也需要将代理返回内容里的https改成http,否则就访问不了了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.request as urllib2
import logging
import re
import gzip
import logging
import sys
from bs4 import BeautifulSoup

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')

fh = logging.FileHandler(sys.path[0] + '/考试.log',mode='a', encoding='utf-8')
fh.setLevel(logging.INFO)
fh.setFormatter(formatter)
logger.addHandler(fh)

ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setFormatter(formatter)
logger.addHandler(ch)

# logging.basicConfig()

class ProxyHandler(BaseHTTPRequestHandler):
target_host = 'kaoshi.wjx.top'

def proxy(self):
header = dict(self.headers)
logger.info('header: %s', header)
# print(header)
if self.path == '/npm/eruda':
self.send_response(200)
self.end_headers()
with open(sys.path[0] + '/eruda.js', 'r', encoding='utf-8') as f:
self.wfile.write(f.read().encode('utf-8'))
return
elif self.path == '/script.js':
self.send_response(200)
self.end_headers()
with open(sys.path[0] + '/script.js', 'r', encoding='utf-8') as f:
self.wfile.write(f.read().encode('utf-8'))
return
else:
header['Host'] = 'kaoshi.wjx.top'
request = urllib2.Request(url='http://' + "101.37.44.53" + self.path, headers=header, method=self.command)
# print(request.full_url)
logger.info('request: %s', request.full_url)
with urllib2.urlopen(request) as f:
# print(f.status, f.headers)
logger.info("status: %s", f.status)
logger.info("headers: %s", f.headers)

self.send_response(f.status)
for (key, value) in f.headers.items():
if key == 'Content-Type':
self.send_header(key, value)
self.end_headers()
data = f.read()
if f.headers['Content-Encoding'] == 'gzip':
data = gzip.decompress(data)
data = data.decode('utf-8')
# print(data)
if 'https' in data:
data = data.replace('https://', 'http://')
logger.info("data: %s", data)
logger.info('<<<<: %s', self.path)
data = get_answer(data)
self.wfile.write(data.encode('utf-8'))


def do_GET(self):
self.proxy()


def do_POST(self):
self.proxy()

def get_answer(data: str):
if '</body>' in data:
i = data.index('</body>')
script = '''
<script src="//kaoshi.wjx.top/npm/eruda"></script><script>setTimeout(() => eruda.init(), 3000);</script>
'''
script = script + '''<script src="//kaoshi.wjx.top/script.js"></script>'''
data = data[:i] + script + data[i:]
return data

def run():
server_address = ('', 80)
httpd = HTTPServer(server_address, ProxyHandler)
httpd.serve_forever()

if __name__ == '__main__':
run()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
document.addEventListener("DOMContentLoaded", (event) => {
let next = document.querySelector('a[onclick="show_next_page();"]');
next.addEventListener('click', (event) => {
event.preventDefault();
show_next_page();
let q = document.querySelector('fieldset[style=""]');
if (q.page != 1) {
t = q.querySelector('div.topichtml').innerHTML;
o = Array.from(q.querySelectorAll('div.label')).map((e) => e.innerHTML);
console.log(t);
console.log(o);
fetch('https://api.dify.ai/v1/chat-messages', {
method: 'POST',
headers: {
'Authorization': 'Bearer xxxxxx',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"inputs": {},
// "query": "党员领导干部的配偶、子女及其配偶,违反有关规定在该党员领导干部管辖的地区和业务范围内从事可能影响其公正执行公务的经营活动,或者有其他违反经商办企业禁止规定行为的,该党员领导干部应当按照规定予以纠正;拒不纠正的,()。\nA.其本人应当辞去现任职务\nB.由组织予以调整职务\nC.其本人应当辞去现任职务或者由组织予以调整职务\nD.直接撤销党内职务处分\n答案是什么?",
"query": t + '\n' + o.join('\n') + '\n答案是什么?',
"response_mode": "blocking",
"conversation_id": "",
"user": "abc-123",
"files": [
]
})
})
.then(response => response.json())
.then(data => {
console.log(data);
r = JSON.parse(data)['answers'];
p = document.createElement('p');
p.innerHTML = r;
q.querySelector('div.topichtml').append(p);
});
}
});
});

rete

原始论文

http://reports-archive.adm.cs.cmu.edu/anon/1995/CMU-CS-95-113.pdf

源码解读

https://github.com/GNaive/naive-rete

问题

由google gemini-pro-1.5回答

在rete算法中,TOKEN与WEM的区别是什么?

在 Rete 算法中,TOKEN 和 WEM (Working Memory Element) 是两个重要的概念,它们在规则引擎的工作机制中扮演着不同的角色。
TOKEN

  • 定义: TOKEN 是一个数据结构,它代表一个事实或事件,可以被规则引擎用来匹配规则条件。
  • 作用: TOKEN 存储了与特定事实或事件相关的信息,并参与规则匹配过程。
  • 特性:
    • 不可变: 一旦创建,TOKEN 的内容通常是不可变的。
    • 携带信息: TOKEN 包含了与事实或事件相关的所有必要信息,例如属性、值等。
    • 匹配规则: TOKEN 被用来匹配规则中的条件,如果匹配成功,则触发相应的规则。

WEM (Working Memory Element)

  • 定义: WEM 是一个数据结构,它代表一个在工作内存中的事实或事件。
  • 作用: WEM 存储了所有当前有效的 Facts 或 Events,并参与规则匹配和更新操作。
  • 特性:
    • 可变: WEM 的内容可以被更新或删除,以反映工作内存中的变化。
    • 跟踪变化: WEM 可以跟踪工作内存中的事实和事件的变化,例如添加、删除或修改。
    • 影响规则: WEM 中的变化可能会影响规则的匹配结果,例如触发新的规则或取消旧的规则。

区别总结:

特征 TOKEN WEM
定义 事实或事件 工作内存中的事实或事件
作用 匹配规则条件 存储事实和事件
可变性 不可变 可变
变化追踪 不追踪 追踪
影响规则 触发规则 影响规则匹配

举例:
假设有一个规则:

1
2
规则: IF A = 1 AND B = 2 THEN C = 3

  • 当一个 TOKEN 携带信息 A = 1 和 B = 2 进入系统时,它会与规则中的条件进行匹配。
  • 如果匹配成功,该 TOKEN 会触发规则,并创建一个新的 WEM 存储 C = 3。
  • 之后,如果另一个 TOKEN 携带信息 A = 2 进入系统,它会与 WEM 中的 C = 3 进行匹配,可能会触发其他规则。

总结:
TOKEN 代表着具体的、不可变的事实或事件信息,用于匹配规则条件。WEM 代表着工作内存中的事实和事件,可变且跟踪变化,影响规则的匹配和执行。两者协同工作,共同构建了 Rete 算法的规则引擎机制。

在rete算法中,join node的作用是什么

在 Rete 算法中,Join Node 是非常重要的组成部分,它的作用是连接匹配不同规则条件的 Token,并将符合条件的 Token 组合起来,传递给后续节点
简单来说,可以将 Join Node 想象成数据库中的 JOIN 操作,它根据一定的条件将来自不同来源的数据合并在一起。
更具体地说,Join Node 的作用体现在以下几个方面:
1. 模式匹配: 每个 Join Node 对应一个规则中的多个条件。当来自不同 Alpha Node (存储单个条件匹配结果的节点) 的 Token 进入 Join Node 时,它会检查这些 Token 是否满足连接条件。
2. 连接条件: Join Node 会存储连接条件,通常是用来比较两个 Token 的属性值是否相等。例如,一个 Join Node 的连接条件可能是 “Token1.customerID == Token2.customerID”,表示只有当两个 Token 的 customerID 属性相等时才会被连接。
3. Token 组合: 如果 Join Node 判断多个 Token 满足连接条件,就会将它们组合成一个新的 Token,这个新的 Token 包含了所有输入 Token 的信息。
4. 传递 Token: Join Node 会将组合后的新 Token 传递给它的后续节点,例如 Beta Node (存储部分或全部规则匹配结果的节点) 或另一个 Join Node。
Join Node 的优势:

  • 提高效率: Join Node 通过提前执行连接操作,避免了在后续节点中进行重复的匹配工作,从而提高了规则匹配的效率。
  • 支持复杂规则: 通过多个 Join Node 的级联,Rete 算法可以处理包含多个条件的复杂规则。

举例说明:
假设有一个规则:

1
2
规则:  IF  Person.city == "北京" AND Order.customerID == Person.customerID THEN ...

这个规则包含两个条件:

  • 条件1: Person.city == “北京”
  • 条件2: Order.customerID == Person.customerID

在 Rete 网络中,这两个条件会被分别存储在两个 Alpha Node 中。当有新的 Person 或 Order 对象进入系统时,对应的 Alpha Node 会生成 Token 并传递给后续的 Join Node。
Join Node 会检查来自两个 Alpha Node 的 Token 是否满足连接条件 “Order.customerID == Person.customerID”,如果满足,就会将它们组合成一个新的 Token,包含 Person 和 Order 的所有信息,并传递给后续节点。
总而言之,Join Node 是 Rete 算法中至关重要的组成部分,它通过连接匹配不同规则条件的 Token,将符合条件的 Token 组合起来,为高效执行复杂规则匹配提供了基础。

在rete算法中,否定条件的节点如何实现

在 Rete 算法中,否定条件的实现依赖于一种特殊的节点类型,称为 Not Node (否定节点) 或 Negative Node。 Not Node 用于处理规则中包含 “NOT”、”!==” 等否定语义的条件。
Not Node 的工作机制:

  1. 接收 Token: Not Node 会从其左侧的节点(通常是 Alpha Node、Beta Node 或另一个 Not Node)接收 Token。这些 Token 代表着已经满足了规则中部分条件的数据。
  2. 监听右侧输入: Not Node 同时还会监听来自其右侧的另一个分支的输入。这个分支通常连接到一个或多个 Alpha Node 或 Beta Node,用于匹配否定条件。
  3. 判断否定条件: 一旦 Not Node 的右侧分支有新的 Token 输入,它会将该 Token 与自身存储的左侧 Token 进行比较。
    • 如果匹配: 表示否定条件成立,Not Node 会撤销之前传递给后续节点的左侧 Token,因为数据已经不再满足规则条件。
    • 如果不匹配: 表示否定条件不成立,Not Node 会将左侧 Token 继续传递给后续节点,因为数据仍然满足规则条件。
  4. 传递 Token: 当 Not Node 的左侧收到新的 Token 时,它会重复步骤 2 和 3,以确保只有满足所有条件(包括否定条件)的 Token 才会被传递到后续节点。

Not Node 的特点:

  • 延迟执行: Not Node 不会立即对其接收到的 Token 进行处理,而是会等待右侧分支的输入,再进行否定条件的判断。
  • 撤销机制: 当否定条件成立时,Not Node 会主动撤销之前传递给后续节点的 Token,以维护数据的一致性。

示例说明:
假设有一条规则:

1
2
规则: IF Person.age > 18 AND NOT Order.product == "玩具" THEN ...

这条规则包含一个否定条件: NOT Order.product == "玩具",表示只有当订单中的商品不是玩具时,规则才会被触发。
在 Rete 网络中,这个规则会被分解成多个节点,其中包含一个 Not Node 用于处理否定条件。

  • 当一个 Person 对象的 age 属性大于 18 时,会生成一个 Token 并传递到 Not Node 的左侧。
  • 当一个 Order 对象的 product 属性等于 “玩具” 时,会生成一个 Token 并传递到 Not Node 的右侧。
  • Not Node 会比较这两个 Token,发现它们匹配(即否定条件成立),因此会撤销之前传递给后续节点的 Person Token。

总结:
Not Node 是 Rete 算法中处理否定条件的关键机制,它通过监听多个分支的输入、比较 Token 并撤销不符合条件的数据,确保了只有满足所有条件(包括否定条件)的数据才会触发规则。

rete算法中,Conjunctive Negations如何实现

在 Rete 算法中,Conjunctive Negations(合取否定)指的是规则中包含多个否定条件,并且这些否定条件之间是 AND 关系的情况。例如:

1
2
规则:IF A > 10 AND NOT B = 20 AND NOT C = 30 THEN ...

这个规则包含两个否定条件:NOT B = 20NOT C = 30, 而且它们之间是 AND 的关系,也就是说只有当 B 不等于 20 并且 C 不等于 30 时,规则才会被触发。
实现 Conjunctive Negations 的关键在于如何协调多个否定条件的匹配结果,确保只有当所有否定条件都满足时才会触发规则。
在 Rete 网络中,通常有以下几种方式实现 Conjunctive Negations:
1. 级联 Not Node:

  • 将多个 Not Node 级联起来,每个 Not Node 负责处理一个否定条件。
  • 第一个 Not Node 接收来自左侧节点(例如 Alpha Node 或 Beta Node)的 Token,并监听其右侧分支(匹配第一个否定条件)的输入。
  • 如果第一个否定条件成立,则第一个 Not Node 会将 Token 传递给第二个 Not Node,否则丢弃该 Token。
  • 第二个 Not Node 重复上述过程,直到所有 Not Node 都满足条件,才会将 Token 传递给后续节点。

这种方式简单直观,但效率较低,因为需要在多个 Not Node 之间传递 Token,增加了网络的复杂度。
2. 专用 Join Node:

  • 使用一个特殊的 Join Node 来处理多个否定条件。
  • 该 Join Node 会接收来自多个 Alpha Node 或 Beta Node 的 Token,分别代表不同的否定条件。
  • Join Node 会存储所有否定条件,并检查输入的 Token 是否与所有否定条件都匹配。
  • 只有当一个 Token 与所有否定条件都匹配时,Join Node 才会将该 Token 传递给后续节点。

这种方式效率较高,因为它避免了在多个节点之间传递 Token,但需要设计专门的 Join Node 来处理否定条件的逻辑。
3. Negation Join Node + 共享 Beta Memory:

  • 使用一种特殊的 Negation Join Node 来连接多个否定条件分支。
  • 每个否定条件分支连接到一个 Beta Node,所有 Beta Node 共享同一个 Beta Memory。
  • 当一个 Token 满足某个否定条件时,会将其加入到共享的 Beta Memory 中。
  • Negation Join Node 会监听共享 Beta Memory 中数据的变化,只有当 Beta Memory 中包含所有否定条件对应的 Token 时,才会将左侧 Token 传递给后续节点。

这种方式结合了前两种方法的优点,既能高效地处理多个否定条件,又能保持 Rete 网络的结构简洁。
总而言之,Conjunctive Negations 是 Rete 算法中处理复杂规则的重要机制。选择哪种实现方式取决于具体的应用场景和性能需求。

使用groovy元编程实现规则引擎

利用groovy的元编程能力,实现以下逻辑。将function和匹配规则存储在数据库中,启动时读取相关数据,替换掉functions字段,实现简单的模式匹配式的规则引擎。

例如

  1. 增加方法

    1
    2
    3
    4
    5
    addFunc "familyStructure" {
    params ->
    def ageDuration = params[0]
    return familyInsureds.count {i -> i.age >= ageDuration[0] && i.age <= ageDuration[1]}
    }
  2. 增加规则

    1
    addAction {familyInsureds.count({i -> i.relation == 3}) > 3} {multiply '1.144'}

    无论函数规则都可以存储在数据库中,且该规则类可以通过缓存动态刷新替换。

问题:

  1. 如何利用rete算法解决模式匹配的低效。
  2. 如何加速groovy代码的执行,解决预热问题。

interrupt

Java线程的interrupt方法本质上是在线程对象上打了一个标记。实际上如果线程本身愿意的话,是可以忽略这个标记的。但是如果线程是处于阻塞状态,那么这个标记会使线程从阻塞状态中退出,并抛出一个InterruptedException异常。这个异常是一个检查异常,所以在调用interrupt方法的时候,要么捕获这个异常,要么抛出这个异常。这个异常的抛出会清除掉线程的中断标记。

利用interrupt可以实现一个可以终止的异步线程管理。

OpenAPI

OpenAPI 规范(OAS)是一种通用的、和编程语言无关的 API 描述规范,使人类和计算机都可以发现和理解服务的功能,而无需访问源代码、文档或针对接口进行嗅探。正确定义后,使用者可以使用最少的实现逻辑来理解远程服务并与之交互。

OpenAPI 始于 Swagger 规范,Swagger 规范已于 2015 年捐赠给 Linux 基金会后改名为 OpenAPI,并定义最新的规范为 OpenAPI 3.0。

可以使用文本编辑器编写yaml按照规范的方式来定义API,也可以使用Swagger Editor在线编辑器来编辑。

定义好的API可以使用APIFOX或者RapiPdf生成接口文档,Swagger Editor也可以生成各种代码的server以及client代码。

更多的相关工具openapi.tools

问题:

  1. 结合json-schema,是否可以实现一个类似soap或者protobuf的接口定义?使用框架生产服务端客户端接口代码,并在框架层面进行参数校验。

WIKI RAG QA

wikiQA是一个问答系统,用于从confluence中获取文档,生成文档向量,然后用llama3-chinese-alpaca回答问题。

wiki_qa.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.chat_models import ChatOllama
from langchain.chains.retrieval import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate

from langchain_community.vectorstores import Chroma

from langchain_community.document_loaders import ConfluenceLoader
import requests
import sys
import os
import json
import pickle
import pathlib
import concurrent.futures
from requests.cookies import cookiejar_from_dict

class WiKi_QA:
def __init__(self):
self.wiki_url = "xxxxx"
self.embedding = OllamaEmbeddings(model='smartcreation/dmeta-embedding-zh:f16')

def gen_vectors(self, jsessionid: str = None, space_key: str = None, page_ids: str = None):
print('gen_vectors', jsessionid, space_key, page_ids)
if not page_ids and space_key == 'healthy' and pathlib.Path(os.path.split(os.path.realpath(__file__))[0] + '/persist').exists():
print('use presist')
self.vectordb = Chroma(persist_directory=os.path.split(os.path.realpath(__file__))[0] + '/persist', embedding_function=self.embedding)
else:
s = requests.Session()
s.cookies = cookiejar_from_dict({
'JSESSIONID': jsessionid
})
# s.proxies = {'http': 'http://127.0.0.1:9999', 'https': 'http://127.0.0.1:9999'}

loader = ConfluenceLoader(
url=self.wiki_url,
session= s,
cloud=False,
# space_key="healthy",
space_key=None if space_key == '' else space_key,
page_ids= page_ids.split(',') if page_ids else None,
limit=1,
max_pages=99999999
)
documents = loader.load()
print(len(documents))
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=100, add_start_index=True
)
texts = text_splitter.split_documents(documents)
print(len(texts))
self.vectordb = Chroma.from_documents(documents=texts, embedding=self.embedding)


def fetch_health_docs(self, jsessionid: str):
with open(sys.path[0] + '/page_ids.json', 'r', encoding='utf-8') as f:
page_ids = json.loads(f.read())

with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
future_to_url = {executor.submit(self.fetch_docs, jsessionid, page_id): page_id for page_id in page_ids}
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
data = future.result()
except Exception as exc:
print('%r generated an exception: %s' % (url, exc))

def fetch_docs(self, jsessionid: str, page_id: str):
print('start', page_id)
s = requests.Session()
s.cookies = cookiejar_from_dict({
'JSESSIONID': jsessionid
})
loader = ConfluenceLoader(
url=self.wiki_url,
session= s,
cloud=False,
page_ids=[page_id,],
limit=1,
max_pages=99999999
)
documents = loader.lazy_load()
index = 0
for one in documents:
print(one)
with open(sys.path[0] + f'/doc/{page_id}_{index}.pkl', 'wb') as f:
pickle.dump(one, f)
index = index + 1

def gen_healthy_vectors(self):
documents = []
for plk in pathlib.Path(sys.path[0] + '/doc').iterdir():
with open(sys.path[0] + f'/doc/{plk.name}', 'rb') as f:
documents.append(pickle.load(f))
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=100, add_start_index=True
)
texts = text_splitter.split_documents(documents)
print(len(texts))
self.vectordb = Chroma.from_documents(documents=texts, embedding=self.embedding, persist_directory=sys.path[0] + '/persist')

def retreival_qa_chain(self):
self.retriever = self.vectordb.as_retriever(search_kwargs={"k":8})
self.llm = ChatOllama(model='lgkt/llama3-chinese-alpaca', temperature=0.)

system_prompt = (
"You are an assistant for question-answering tasks. "
"Use the following pieces of retrieved context to answer "
"the question. If you don't know the answer, say that you "
"don't know. Use three sentences maximum and keep the "
"answer concise."
"\n\n"
"{context}"
)

prompt = ChatPromptTemplate.from_messages(
[
("system", system_prompt),
("human", "{input}"),
]
)

question_answer_chain = create_stuff_documents_chain(self.llm, prompt)
self.chain = create_retrieval_chain(self.retriever, question_answer_chain)

def answer_confluence(self,question:str) ->str:
answer = self.chain.invoke({"input": question})
return answer

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import streamlit as st
from wiki_qa import WiKi_QA

st.set_page_config(
page_title='Wiki Q&A',
page_icon='📚📚📚📚',
layout='wide',
initial_sidebar_state='auto',
)
if "config" not in st.session_state:
st.session_state["config"] = {}
if "qa" not in st.session_state:
st.session_state["qa"] = None

@st.cache_resource
def load_confluence(config):
qa = WiKi_QA()
qa.gen_vectors(config['jsession_id'], config['space_key'], config['page_ids'])
qa.retreival_qa_chain()
return qa

with st.sidebar.form(key ='Form1'):
st.markdown('## 使用配置')
jsession_id = st.text_input(label="jsessionid",
help="F12获取jsessionid")
space_key = st.text_input(label="空间",
help="wiki的空间",
value="healthy")
page_ids = st.text_input(label="页面id",
help="多个页面id用逗号分隔")
submitted1 = st.form_submit_button(label='Submit')

if submitted1:
st.session_state["config"] = {
"jsession_id": jsession_id if jsession_id != "" else None,
"page_ids": page_ids if page_ids != "" else None,
"space_key": space_key,
}
with st.spinner(text="..."):
config = st.session_state["config"]
st.session_state["config"] = config
st.session_state["qa"] = load_confluence(st.session_state["config"])
st.write("Ingested")


st.title("WIKI Q&A")

question = st.text_input('问一个问题', "商品中心有哪两部分组成?")

if st.button('获取答案', key='button2'):
with st.spinner(text="..."):
qa = st.session_state.get("qa")
if qa is not None:
result = qa.answer_confluence(question)
st.write(result)
else:
st.write("请先设置")

llama3回答都是英文,因而改用lgkt/llama3-chinese-alpaca,但中文返回结果感觉比较单薄。

问题:

  1. 为什么要用RecursiveCharacterTextSplitter?与其他的splitter有什么区别?还有哪些splitter?chunk_sizechunk_overlap如何设置?
  2. retriever返回的结果有时明显不准确,如何提高准确率?
  3. embedding的model如何选择?对中文有影响吗?

Build a Retrieval Augmented Generation (RAG) App
Building a Confluence Q&A App with LangChain and ChatGPT
RAG行业交流中发现的一些问题和改进方法