BPO現場をコードでモデリングしてみる
「BPOって、外注のデータ入力とかコールセンターでしょ?」
よくこう言われるんですが、
自分のいた世界を一言で表すと、こうなります。
自分のいた世界は、ソフトウェア化されてないソフトウェアの世界だった。
画面もコードも出てこないのに、
そこにはちゃんと「業務フロー」「ルール」「エラーハンドリング」「データモデル」がありました。
ただ、それを実行していたのが人間マシンだった、というだけです。
人間マシンというランタイム
現場で働くオペレーターを、あえて「マシン」として見てみます。
- マニュアル = インストールするアプリケーション
- 人ごとのスキル・経験 = OS / ハードウェア差
- その日の体調・忙しさ = 環境変数
同じマニュアルをインストールしても、
- OS(認知の癖・経験)
- CPU/RAM(処理速度・ワーキングメモリ)
- env(その日の忙しさ・気分)
といった実行環境がそれぞれ違うため、
インストール結果(=ふるまい)はバラつきます。
このランタイムは、ブラウザの JavaScript のように
人ごとに実行エンジンが違う と捉えるとわかりやすいです。
その結果、現場では日常的に次のような現象が起こります。
- 本来エラーにすべき値を、勝手に別の値へ変換して処理を続けてしまう
- ブラウザキャッシュのように、覚えているマニュアル(経験)が人ごとに違う
- JavaScript の言語バージョン差のように、解釈が微妙に揺れる
人間マシンとは、
統一されたサーバーではなく、バラバラのブラウザが勝手に動いている状態に近い
存在なんです。
業務フローは分散システムだ
こういう人間マシンたちをたくさん並べて、
FAXから流れ込む注文を処理する。
この構図、もはや分散システムです。
class Workflow {
constructor(private operators: HumanMachine[]) {}
run(inputFax: FaxImage): ProcessResult[] {
const docs = this.preprocess(inputFax); // 目視(OCR)など
return docs.map((doc) => this.dispatch(doc)); // キュー投入 & 担当割当
}
private dispatch(doc: Document): ProcessResult {
const operator = this.assignOperator(doc); // 誰に投げるか
return operator.process(doc); // 人間マシンにリクエスト
}
}
abstract class HumanMachine {
constructor(protected manual: Manual) {} // **eval(manual)**
process(doc: Document): ProcessResult {
const parsed = this.parse(doc);
const validated = this.validate(parsed);
return this.handle(validated);
}
protected abstract parse(doc: Document): ParsedData;
protected abstract validate(data: ParsedData): ValidatedData;
protected abstract handle(data: ValidatedData): ProcessResult;
}
自分がやっていた仕事を端的に言うと、
この Workflow と HumanMachine の設計とリファクタリング を
自然言語でやっていた
という感覚に近いです。
FAXの入力は loosely-typed な Webhook の集合
入力は紙のFAXです。
IT エンジニアっぽく言うと、だいたいこんな仕様でした。
- エンドポイント:Post / FAX
- ペイロード:手書き、自由なレイアウト
- スキーマ:なし
- バリデーション:目視・運用ルール
// loosely-typed な Webhook 群
// Webhook エンドポイント
POST /fax
// 壊れた Webhook のような外部入力
type FaxPayload = UnstructuredImageInput; // 画像+手書き
// 人力 OCR して、ようやくここまで持っていける
type OcrOutput = {
rawText: string | unknown; // 人力 OCR でも unknown が混じる
layoutBlocks: unknown[];
noise: unknown;
};
// 不正なテキストは後続処理で異常系へ
このような「謎のペイロード」を扱うために、
人間マシンが目視で解析・バリデーション・エラーハンドリングを行い、
社内システムが要求する入力形式へと変換します。
業務フローを「分散パイプライン」に再設計した話
現場では最初、ひとりのチェック担当(Checker)が
- 得意先
- 日付
- 商品
- 数量
など、全部まとめて見る巨大メソッドになっていました。
// Before: 何でも詰め込まれた巨大チェック担当
class Checker extends HumanMachine {
customerOk(order: Order): boolean { /* ... */ }
dateOk(order: Order): boolean { /* ... */ }
itemOk(order: Order): boolean { /* ... */ }
run(order: Order): boolean {
return (
this.customerOk(order) &&
this.dateOk(order) &&
this.itemOk(order)
);
}
}
自分がやっていたのは、これを
「役割ごとに分割された人間マシンのパイプライン」に
再設計・リファクタリングする仕事に近いです。
// After: 関心ごとに分割された純粋関数っぽいチェック担当
class CustomerChecker {
check(input: Order): OrderWithCustomer { /* 得意先だけ見る */ }
}
class DateChecker {
check(input: OrderWithCustomer): OrderWithDate { /* 日付だけ見る */ }
}
class ItemChecker {
check(input: OrderWithDate): OrderWithItems { /* 商品だけ見る */ }
}
class QtyChecker {
check(input: OrderWithItems): FinalOrder { /* 数量だけ見る */ }
}
// 分散パイプラインとしての業務ライン
// 人間マシンそれぞれへ渡して実行する
const run = (input: Order) =>
pipe(
input,
(o) => customerChecker.check(o),
(o) => dateChecker.check(o),
(o) => itemChecker.check(o),
(o) => qtyChecker.check(o),
);
得意先チェック単体でみると、
- 得意先名と住所の一致だけを見る CustomerChecker
- 日付や商品・数量は、別の人間マシンに委譲する
というふうに、「1人で全部見る巨大クラス」から
「単一責務の小さなクラスをつないだ分散パイプライン」へと変えていくイメージです。
だから今、コードでこの世界地図を描きたい
振り返ってみると、BPOでやっていたことは
- ドメインモデリング(「本当の得意先」とは何か?)
- エラーハンドリング設計(どこで誰にエスカレーションするか)
- 状態遷移の整理(受付 → 処理中 → 保留 → 完了)
- 認知負荷の分散(人に持たせるコンテキストを絞る)
- データモデルの正規化(1枚のFAXに詰め込まれた情報を分解する)
といった、エンジニアリングの前段階のような仕事だったのだと思います。
ソフトウェア化されてないソフトウェアの世界で、
ずっと「型がない状態管理」と戦ってきた反動で、
- Rust や TypeScript のような型のある言語
- 代数データ型 (ADT) / Result / Option のような表現力
に強く惹かれるようになりました。
人間マシンが実行してきたロジックを、
これからはコードとして再設計していきたい。
そんな思いで、今は Web エンジニアとしてのキャリアに舵を切っています。
この「ソフトウェア化されてないソフトウェアの世界地図」を、
一緒にコードで描き直していけるチームに出会えたらうれしいです。